├── .github └── workflows │ └── gradle_ci.yml ├── .gitignore ├── .run └── local-publish.run.xml ├── LICENSE ├── README.md ├── build.gradle.kts ├── buildSrc ├── build.gradle.kts ├── settings.gradle.kts └── src │ └── main │ └── java │ └── de │ └── honoka │ └── gradle │ └── buildsrc │ ├── DependencyHandler.kt │ ├── KotlinDslCopy.kt │ ├── MavenPublish.kt │ └── Project.kt ├── docs ├── changelog.md └── img │ ├── 1.png │ ├── 2.png │ ├── 3.png │ └── badge │ ├── Java-17-brightgreen.svg │ └── Java-8-brightgreen.svg ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── honoka-kotlin-utils ├── build.gradle.kts └── src │ └── main │ ├── java │ └── de │ │ └── honoka │ │ └── sdk │ │ └── util │ │ └── kotlin │ │ ├── basic │ │ ├── BooleanBuilder.kt │ │ ├── Collections.kt │ │ ├── Exceptions.kt │ │ ├── Logger.kt │ │ ├── PartialAbstract.kt │ │ ├── PropertyValueContainer.kt │ │ └── Various.kt │ │ ├── concurrent │ │ ├── ScheduledTask.kt │ │ ├── Thread.kt │ │ └── ThreadPoolUtilsExt.kt │ │ ├── io │ │ ├── ByteBufferIoStream.kt │ │ └── MiddleIoStream.kt │ │ ├── net │ │ ├── http │ │ │ ├── Http.kt │ │ │ └── HttpUtilExt.kt │ │ └── socket │ │ │ └── SocketUtils.kt │ │ ├── text │ │ ├── Json.kt │ │ ├── JsonWrapper.kt │ │ ├── StringWrapper.kt │ │ ├── Strings.kt │ │ └── Xml.kt │ │ └── various │ │ └── RuntimeUtilsExt.kt │ └── resources │ └── http │ ├── .gitkeep │ └── static-headers │ ├── api.json │ └── document.json ├── honoka-spring-boot-starter ├── build.gradle.kts └── src │ └── main │ ├── java │ └── de │ │ └── honoka │ │ └── sdk │ │ └── spring │ │ └── starter │ │ ├── HonokaStarter.kt │ │ ├── config │ │ ├── MainConfig.kt │ │ ├── MybatisPlusConfig.kt │ │ └── SecurityConfig.kt │ │ ├── core │ │ ├── aop │ │ │ └── AspectUtils.java │ │ ├── context │ │ │ ├── ApplicationContextHolder.kt │ │ │ ├── ConditionalComponent.kt │ │ │ └── Config.kt │ │ ├── database │ │ │ └── AbstractEmbeddedDatabaseUtils.java │ │ ├── gui │ │ │ └── SpringBootConsoleWindow.java │ │ └── web │ │ │ ├── GlobalExceptionHandler.kt │ │ │ ├── HttpServlet.kt │ │ │ └── WebUtils.kt │ │ ├── mybatis │ │ └── QueryWrapper.kt │ │ └── security │ │ ├── DefaultAuthorizationFilter.kt │ │ ├── DefaultUser.kt │ │ ├── SecurityExceptionHandler.kt │ │ └── token │ │ ├── JwtUtils.kt │ │ └── TempTokenUtils.kt │ └── resources │ └── META-INF │ ├── .gitkeep │ └── spring │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports ├── honoka-utils ├── build.gradle.kts └── src │ └── main │ ├── java │ └── de │ │ └── honoka │ │ └── sdk │ │ └── util │ │ ├── basic │ │ ├── ActionUtils.java │ │ ├── CodeUtils.java │ │ ├── ColorfulText.java │ │ ├── DateBuilder.java │ │ ├── HonokaComparator.java │ │ ├── ThrowsConsumer.java │ │ ├── ThrowsRunnable.java │ │ └── javadoc │ │ │ ├── NotThreadSafe.java │ │ │ └── ThreadSafe.java │ │ ├── concurrent │ │ ├── LockUtils.java │ │ ├── NewThreadFirstQueue.java │ │ └── ThreadPoolUtils.java │ │ ├── file │ │ ├── AbstractEnvironmentPathUtils.java │ │ ├── FileUtils.java │ │ └── csv │ │ │ ├── CsvTable.java │ │ │ └── DefaultCsvTable.java │ │ ├── gui │ │ ├── ColorAttributeSets.java │ │ ├── ColorfulOutputStream.java │ │ ├── ConsoleInputStream.java │ │ ├── ConsoleOutputStream.java │ │ ├── ConsoleWindow.java │ │ ├── ConsoleWindowBuilder.java │ │ └── WrapableJTextPane.java │ │ ├── text │ │ ├── EmojiHelper.java │ │ ├── EncodingUtils.java │ │ ├── HtmlUtils.java │ │ ├── TextUtils.java │ │ └── XmlUtils.java │ │ ├── various │ │ ├── ImageUtils.java │ │ ├── ListRunner.java │ │ ├── ReflectUtils.java │ │ ├── Retrier.java │ │ ├── RuntimeUtils.java │ │ ├── SystemEnum.java │ │ └── SystemInfoBean.java │ │ └── web │ │ └── ApiResponse.java │ └── resources │ ├── img │ └── java.png │ └── text.html ├── scripts ├── github │ ├── build.sh │ └── publish.sh └── local │ ├── local-publish.bat │ └── local-publish.sh └── settings.gradle.kts /.github/workflows/gradle_ci.yml: -------------------------------------------------------------------------------- 1 | name: Gradle CI 2 | 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | outputs: 7 | IS_DEVELOPMENT_VERSION: ${{ steps.gradle_build.outputs.is_development_version }} 8 | steps: 9 | - uses: actions/checkout@v4 10 | with: 11 | submodules: 'recursive' 12 | - name: Set up JDK 17 13 | uses: actions/setup-java@v4 14 | with: 15 | java-version: '17' 16 | distribution: 'temurin' 17 | # 通过预定义的包管理器之一管理的依赖项的快速设置缓存 18 | # 它可以是maven、gradle或sbt之一 19 | cache: 'gradle' 20 | - id: gradle_build 21 | name: Build with Gradle 22 | run: | 23 | chmod +x ./scripts/github/*.sh 24 | ./scripts/github/build.sh ${{ secrets.REMOTE_MAVEN_REPO_URL }} 25 | - name: Upload remote maven repository as artifact 26 | uses: actions/upload-artifact@v4 27 | with: 28 | # 指定要上传的目录在上传到当前Actions运行实例的Artifacts当中时要使用的名字 29 | name: remote-maven-repo-copy 30 | # 要上传的目录路径(以项目根目录为相对路径起始点) 31 | path: remote-maven-repo-copy 32 | publish: 33 | runs-on: ubuntu-latest 34 | needs: build 35 | steps: 36 | - uses: actions/checkout@v4 37 | with: 38 | submodules: 'recursive' 39 | - name: Download temporary maven repository 40 | uses: actions/download-artifact@v4 41 | with: 42 | # 指定在之前的job当中,上传到Actions运行实例的Artifacts当中的目录所使用的名字 43 | name: remote-maven-repo-copy 44 | # 指定要将这一目录下的所有内容(即不包含目录本身)下载到哪个位置(以项目根目录为相对路径起始点) 45 | path: remote-maven-repo-copy 46 | - name: Merge maven repository and publish 47 | env: 48 | IS_DEVELOPMENT_VERSION: ${{ needs.build.outputs.IS_DEVELOPMENT_VERSION }} 49 | run: | 50 | chmod +x ./scripts/github/*.sh 51 | ./scripts/github/publish.sh ${{ secrets.REMOTE_MAVEN_REPO_URL }} 52 | 53 | on: 54 | push: 55 | branches: 56 | - dev 57 | paths-ignore: 58 | - 'docs/**' 59 | - 'gradle/wrapper/**' 60 | - '**src/test/**' 61 | - '.gitignore' 62 | - 'gradlew*' 63 | - 'LICENSE' 64 | - 'README.md' 65 | pull_request: 66 | branches: 67 | - dev 68 | paths-ignore: 69 | - 'docs/**' 70 | - 'gradle/wrapper/**' 71 | - '**src/test/**' 72 | - '.gitignore' 73 | - 'gradlew*' 74 | - 'LICENSE' 75 | - 'README.md' 76 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle/ 2 | build/ 3 | target/ 4 | gradle-build/ 5 | !gradle/wrapper/gradle-wrapper.jar 6 | !**/src/main/**/build/ 7 | !**/src/test/**/build/ 8 | 9 | ### IntelliJ IDEA ### 10 | .idea/ 11 | *.iws 12 | *.iml 13 | *.ipr 14 | out/ 15 | !**/src/main/**/out/ 16 | !**/src/test/**/out/ 17 | 18 | ### Eclipse ### 19 | .apt_generated 20 | .classpath 21 | .factorypath 22 | .project 23 | .settings 24 | .springBeans 25 | .sts4-cache 26 | bin/ 27 | !**/src/main/**/bin/ 28 | !**/src/test/**/bin/ 29 | 30 | ### NetBeans ### 31 | /nbproject/private/ 32 | /nbbuild/ 33 | /dist/ 34 | /nbdist/ 35 | /.nb-gradle/ 36 | 37 | ### VS Code ### 38 | .vscode/ 39 | 40 | ### Mac OS ### 41 | .DS_Store -------------------------------------------------------------------------------- /.run/local-publish.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Honoka SDK 2 | ![Java](./docs/img/badge/Java-8-brightgreen.svg) 3 | ![Kotlin](https://img.shields.io/badge/Kotlin-1.8.10-brightgreen?logo=Kotlin)
4 | [![License](https://img.shields.io/github/license/kosaka-bun/honoka-sdk?label=License&color=blue&logo=GitHub)](./LICENSE) 5 | ![GitHub Stars](https://img.shields.io/github/stars/kosaka-bun/honoka-sdk?label=Stars&logo=GitHub&style=flat) 6 | [![Release](https://img.shields.io/github/release/kosaka-bun/honoka-sdk?label=Release&logo=GitHub)](../../releases) 7 | 8 | ## 简介 9 | Honoka SDK是一款包含了各式各样实用工具的Java与Kotlin工具包,主要包含honoka-utils、honoka-kotlin-utils等模块。提供了包括简写代码、读取文件、后台运行jar包、读取CSV表格、处理Emoji、文字转图片、反射操作等功能的诸多工具类。 10 | 11 | 本项目采用Apache-2.0 License,使用本项目时,请遵守此开源许可证的相关规定。 12 | 13 | **本项目中的所有代码并未经过严格测试,请勿用于生产环境。** 14 | 15 | 请参阅:[更新日志](./docs/changelog.md) 16 | 17 | ## 功能展示 18 | ### [ColorfulText](./honoka-utils/src/main/java/de/honoka/sdk/util/code/ColorfulText.java) 19 | 用于方便地在控制台输出彩色文字。 20 | ```java 21 | ColorfulText.of().red("red ").green("green ").blue("blue").println(); 22 | ``` 23 | ![](./docs/img/1.png) 24 | 25 | ### [ConsoleWindow](./honoka-utils/src/main/java/de/honoka/sdk/util/gui/ConsoleWindow.java) 26 | 这是一个使用Java AWT与Swing等组件编写的一个控制台窗口,它可以通过系统托盘图标的方式,使任何可执行jar包能够在系统后台运行。 27 | 28 | 控制台窗口关闭后,jar包将继续保持在后台运行,点击jar包对应的系统托盘图标,可再次打开控制台窗口。 29 | ```java 30 | public static void main(String[] args) { 31 | //of方法参数:控制台窗口标题 32 | //setOnExit:执行托盘右键菜单中的“退出”项时,要执行的代码段 33 | /* 34 | * setScreenZoomScale:当前系统的屏幕缩放比例大小,用于准确定位右键弹出菜单 35 | * 若不正确设置,则弹出菜单的位置可能会与鼠标点击的位置存在较大偏差 36 | */ 37 | ConsoleWindow.Builder.of("Test").setOnExit(() -> { 38 | System.out.println("系统退出"); 39 | }).setScreenZoomScale(1.25).build(); 40 | SpringApplication.run(TestApplication.class, args); 41 | } 42 | ``` 43 | 控制台窗口: 44 | 45 | ![](./docs/img/2.png) 46 | 47 | 系统托盘图标: 48 | 49 | ![](./docs/img/3.png) 50 | 51 | ### [FileUtils](./honoka-utils/src/main/java/de/honoka/sdk/util/file/FileUtils.java) 52 | #### copyResourceIfNotExists() 53 | 检查当前运行的jar包外部是否含有指定的资源文件,若有则忽略此资源,若没有,则从jar包中指定的相对路径处,提取此资源复制到jar包外部相同的相对路径处。 54 | 55 | 例如,设指定的路径为`/dir/file.txt`,则先判断jar包所在目录下是否存在`./dir/file.txt`文件,若没有,则从指定类所在的jar包中提取`/dir/file.txt`文件,放置于当前jar包目录下的`./dir/file.txt`这一位置。 56 | ```java 57 | //参数1:要提取的资源所在的jar包中的某个类,用于基于它获取资源的URL 58 | //参数2:要提取的资源路径,可以为多个 59 | FileUtils.copyResourceIfNotExists(ClassInJar.class, "/dir/file1.txt", "/dir/file2.txt"); 60 | ``` 61 | 执行完上述代码后,当前运行的jar包所在目录下的`dir`文件夹下应该有`file1.txt`与`file2.txt`两个文件。 62 | 63 | #### getClasspath() 64 | 用于获取当前运行的jar包所在的目录的路径,或所运行的class文件的classpath根目录。 65 | 66 | 设一个Gradle项目在不打包的情况下,直接运行时,其根目录的路径为:`C:\Projects\a-project`。 67 | 68 | 则这个方法的返回值类似于: 69 | ``` 70 | C:\Projects\a-project\build\classes\java\main 71 | ``` 72 | 当某个可执行jar包在执行此方法时,此方法的返回值即为jar包所在的目录的路径。 73 | 74 | #### urlToString() 75 | 将一个URL所对应的资源直接转换为字符串,编码采用JVM运行时的默认编码。 76 | 77 | 主要用于直接读取jar包中的文本文件。 78 | 79 | ### [ImageUtils](./honoka-utils/src/main/java/de/honoka/sdk/util/various/ImageUtils.java) 80 | 文字转图片工具类,转换时图片的高度是自适应的,图片的宽度可以自行指定。可以指定每行最大字符数,或是为图片指定一个固定宽度。此外,它还具有一定的渲染HTML的能力。 81 | 82 | ### [ReflectUtils](./honoka-utils/src/main/java/de/honoka/sdk/util/various/ReflectUtils.java) 83 | 强大的反射工具类,可以方便地获取和修改被`private`和`final`所修饰的字段的值,以及方便地自动查找和调用指定名称的`private`方法,具有一定的类型推断能力。 84 | 85 | ```java 86 | //获取某个定义的字段,并调整可访问性,默认移除final修饰符(如果有) 87 | Field getField(Class clazz, String fieldName); 88 | //设置某个对象的一个成员的值 89 | void setFieldValue(Object obj, String fieldName, Object value); 90 | //设置static字段的值 91 | void setFieldValue(Class clazz, String fieldName, Object value); 92 | //调用某个方法,根据参数值列表,自动推断要查找的方法的参数类型列表 93 | Object invokeMethod(Object obj, String methodName, Object... args); 94 | //调用某个方法 95 | Object invokeMethod(Object obj, String methodName, Class[] parameterType, Object... args); 96 | ``` 97 | 使用此工具类,可以避免手动查找获取字段和方法,以及调用`setAccessible()`的麻烦。 98 | 99 | ### 更多实用工具 100 | 请阅读源代码以了解更多实用工具类的功能。 101 | 102 | ## 使用 103 | 本项目部署于: 104 | 105 | [![maven-repo](https://github-readme-stats.vercel.app/api/pin/?username=kosaka-bun&repo=maven-repo)](https://github.com/kosaka-bun/maven-repo) 106 | 107 | 使用前请先阅读此仓库的文档,为你的Maven或Gradle添加依赖仓库。 108 | 109 | 各模块版本号请前往[Releases](../../releases)查看。 110 | 111 | ### Maven 112 | ```xml 113 | 114 | 115 | de.honoka.sdk 116 | honoka-utils 117 | 版本号 118 | 119 | 120 | 121 | de.honoka.sdk 122 | honoka-kotlin-utils 123 | 版本号 124 | 125 | 126 | ``` 127 | 128 | ### Gradle 129 | #### Groovy DSL 130 | ```groovy 131 | dependencies { 132 | implementation 'de.honoka.sdk:honoka-utils:版本号' 133 | implementation 'de.honoka.sdk:honoka-kotlin-utils:版本号' 134 | } 135 | ``` 136 | 137 | #### Kotlin DSL 138 | ```kotlin 139 | dependencies { 140 | implementation("de.honoka.sdk:honoka-utils:版本号") 141 | implementation("de.honoka.sdk:honoka-kotlin-utils:版本号") 142 | } 143 | ``` 144 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import de.honoka.gradle.buildsrc.MavenPublish.defineCheckVersionOfProjectsTask 2 | import de.honoka.gradle.buildsrc.kotlin 3 | import de.honoka.gradle.buildsrc.projects 4 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 5 | import java.nio.charset.StandardCharsets 6 | 7 | @Suppress("DSL_SCOPE_VIOLATION") 8 | plugins { 9 | java 10 | `java-library` 11 | `maven-publish` 12 | alias(libs.plugins.dependency.management) 13 | alias(libs.plugins.kotlin) apply false 14 | alias(libs.plugins.kotlin.kapt) 15 | alias(libs.plugins.kotlin.lombok) apply false 16 | } 17 | 18 | group = "de.honoka.sdk" 19 | version = libs.versions.root.get() 20 | 21 | //纯Java项目 22 | val javaProjects = projects("honoka-utils") 23 | 24 | //非Java 8项目 25 | val notJava8Projects = projects("honoka-spring-boot-starter") 26 | 27 | subprojects { 28 | apply(plugin = "java") 29 | apply(plugin = "java-library") 30 | apply(plugin = "maven-publish") 31 | apply(plugin = "io.spring.dependency-management") 32 | 33 | val libs = rootProject.libs 34 | 35 | group = rootProject.group 36 | 37 | java { 38 | if(project !in notJava8Projects) { 39 | sourceCompatibility = JavaVersion.VERSION_1_8 40 | targetCompatibility = sourceCompatibility 41 | } 42 | withSourcesJar() 43 | } 44 | 45 | dependencies { 46 | libs.lombok.let { 47 | compileOnly(it) 48 | annotationProcessor(it) 49 | testCompileOnly(it) 50 | testAnnotationProcessor(it) 51 | } 52 | testImplementation("org.junit.jupiter:junit-jupiter-api:5.8.1") 53 | testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.8.1") 54 | } 55 | 56 | //Kotlin项目 57 | if(project !in javaProjects) { 58 | apply(plugin = "org.jetbrains.kotlin.jvm") 59 | apply(plugin = "org.jetbrains.kotlin.kapt") 60 | apply(plugin = "org.jetbrains.kotlin.plugin.lombok") 61 | dependencyManagement { 62 | imports { 63 | mavenBom(libs.kotlin.bom.get().toString()) 64 | } 65 | } 66 | dependencies { 67 | kotlin(project) 68 | //仅用于避免libs.versions.toml中产生version变量未使用的提示 69 | libs.versions.kotlin.coroutines 70 | } 71 | tasks { 72 | withType { 73 | kotlinOptions { 74 | jvmTarget = java.sourceCompatibility.toString() 75 | freeCompilerArgs += listOf( 76 | "-Xjsr305=strict", 77 | "-Xjvm-default=all" 78 | ) 79 | } 80 | } 81 | } 82 | kapt { 83 | keepJavacAnnotationProcessors = true 84 | } 85 | } 86 | 87 | tasks { 88 | compileJava { 89 | options.run { 90 | encoding = StandardCharsets.UTF_8.name() 91 | val compilerArgs = compilerArgs as MutableCollection 92 | compilerArgs += listOf( 93 | "-parameters" 94 | ) 95 | } 96 | } 97 | 98 | test { 99 | useJUnitPlatform() 100 | } 101 | } 102 | 103 | publishing { 104 | repositories { 105 | mavenLocal() 106 | } 107 | } 108 | } 109 | 110 | defineCheckVersionOfProjectsTask() 111 | -------------------------------------------------------------------------------- /buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` 3 | } 4 | 5 | dependencies { 6 | implementation("org.jetbrains.kotlin:kotlin-gradle-plugin-api:1.8.10") 7 | } -------------------------------------------------------------------------------- /buildSrc/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage") 2 | 3 | dependencyResolutionManagement { 4 | repositories { 5 | maven("https://maven.aliyun.com/repository/public") 6 | mavenCentral() 7 | maven("https://mirrors.honoka.de/maven-repo/release") 8 | } 9 | } 10 | 11 | pluginManagement { 12 | repositories { 13 | maven("https://maven.aliyun.com/repository/gradle-plugin") 14 | mavenCentral() 15 | gradlePluginPortal() 16 | } 17 | } -------------------------------------------------------------------------------- /buildSrc/src/main/java/de/honoka/gradle/buildsrc/DependencyHandler.kt: -------------------------------------------------------------------------------- 1 | package de.honoka.gradle.buildsrc 2 | 3 | import org.gradle.api.Project 4 | import org.gradle.api.artifacts.Dependency 5 | import org.gradle.api.artifacts.dsl.DependencyHandler 6 | import org.gradle.api.internal.catalog.VersionModel 7 | 8 | fun DependencyHandler.kotlin(project: Project) { 9 | val versions: Map = project.libVersions() 10 | implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:${versions.getVersion("kotlin")}") 11 | implementation("org.jetbrains.kotlin:kotlin-reflect:${versions.getVersion("kotlin")}") 12 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:${versions.getVersion("kotlin.coroutines")}") 13 | } 14 | 15 | fun DependencyHandler.implementationApi(dn: Any): Dependency? = run { 16 | implementation(dn) 17 | api(dn) 18 | } 19 | -------------------------------------------------------------------------------- /buildSrc/src/main/java/de/honoka/gradle/buildsrc/KotlinDslCopy.kt: -------------------------------------------------------------------------------- 1 | package de.honoka.gradle.buildsrc 2 | 3 | import org.gradle.api.Action 4 | import org.gradle.api.Project 5 | import org.gradle.api.artifacts.Dependency 6 | import org.gradle.api.artifacts.dsl.DependencyHandler 7 | import org.gradle.api.publish.PublishingExtension 8 | 9 | fun Project.publishing(configure: Action) { 10 | extensions.configure("publishing", configure) 11 | } 12 | 13 | fun DependencyHandler.implementation(dn: Any): Dependency? = run { 14 | add("implementation", dn) 15 | } 16 | 17 | fun DependencyHandler.api(dn: Any): Dependency? = run { 18 | add("api", dn) 19 | } 20 | -------------------------------------------------------------------------------- /buildSrc/src/main/java/de/honoka/gradle/buildsrc/MavenPublish.kt: -------------------------------------------------------------------------------- 1 | package de.honoka.gradle.buildsrc 2 | 3 | import org.gradle.api.Project 4 | import org.gradle.api.artifacts.Dependency 5 | import org.gradle.api.publish.maven.MavenPublication 6 | import org.gradle.kotlin.dsl.create 7 | import org.gradle.kotlin.dsl.get 8 | import org.gradle.kotlin.dsl.maven 9 | 10 | object MavenPublish { 11 | 12 | private lateinit var rootProject: Project 13 | 14 | private val projectsWillPublish = ArrayList() 15 | 16 | fun Project.setupVersionAndPublishing(version: String) { 17 | val project = this 18 | this.version = version 19 | publishing { 20 | repositories { 21 | val isReleaseVersion = version.isReleaseVersion() 22 | val isDevelopmentRepository = properties["isDevelopmentRepository"]?.toString() == "true" 23 | if(isReleaseVersion == isDevelopmentRepository) return@repositories 24 | val remoteUrl = properties["remoteMavenRepositoryUrl"]?.toString() ?: return@repositories 25 | maven(remoteUrl) 26 | } 27 | publications { 28 | create("maven") { 29 | groupId = group as String 30 | artifactId = project.name 31 | this.version = version 32 | from(components["java"]) 33 | } 34 | } 35 | } 36 | projectsWillPublish.add(this) 37 | } 38 | 39 | fun Project.defineCheckVersionOfProjectsTask() { 40 | this@MavenPublish.rootProject = rootProject 41 | tasks.register("checkVersionOfProjects") { 42 | group = "publishing" 43 | doLast { 44 | checkVersionOfProjects() 45 | } 46 | } 47 | } 48 | 49 | private fun checkVersionOfProjects() { 50 | var projectsPassed = true 51 | val dependencies = HashSet() 52 | println("Versions:\n") 53 | listOf(rootProject, *projectsWillPublish.toTypedArray()).forEach { 54 | if(!projectsPassed) return@forEach 55 | //若project未设置version,则这里取到的version值为unspecified 56 | println("${it.name}=${it.version}") 57 | dependencies.addAll(it.rawDependencies) 58 | projectsPassed = it.version.isReleaseVersion() 59 | } 60 | val dependenciesPassed = checkVersionOfDependencies(dependencies) 61 | println("\nResults:\n") 62 | println("results.projectsPassed=$projectsPassed") 63 | println("results.dependenciesPassed=$dependenciesPassed") 64 | println("results.passed=${projectsPassed && dependenciesPassed}") 65 | } 66 | 67 | private fun Any?.isReleaseVersion(): Boolean = toString().lowercase().run { 68 | !(isEmpty() || this == "unspecified" || contains("dev")) 69 | } 70 | 71 | private fun checkVersionOfDependencies(dependencies: Set): Boolean { 72 | var passed = true 73 | println("\nDependencies:\n") 74 | dependencies.forEach { 75 | if(!passed) return@forEach 76 | println("${it.group}:${it.name}=${it.version}") 77 | passed = it.version.isReleaseVersion() 78 | } 79 | return passed 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /buildSrc/src/main/java/de/honoka/gradle/buildsrc/Project.kt: -------------------------------------------------------------------------------- 1 | package de.honoka.gradle.buildsrc 2 | 3 | import org.gradle.api.Project 4 | import org.gradle.api.artifacts.ConfigurationContainer 5 | import org.gradle.api.artifacts.Dependency 6 | import org.gradle.api.internal.artifacts.dsl.dependencies.DefaultDependencyHandler 7 | import org.gradle.api.internal.catalog.VersionModel 8 | 9 | @Suppress("UNCHECKED_CAST") 10 | fun Project.libVersions(): Map { 11 | val libs = rootProject.extensions.getByName("libs") 12 | val versions = libs.javaClass.getDeclaredMethod("getVersions").invoke(libs) 13 | val catalog = versions.javaClass.superclass.getDeclaredField("config").run { 14 | isAccessible = true 15 | get(versions) 16 | } 17 | catalog.javaClass.getDeclaredField("versions").run { 18 | isAccessible = true 19 | return get(catalog) as Map 20 | } 21 | } 22 | 23 | fun Map.getVersion(key: String): String = get(key)?.version.toString() 24 | 25 | val Project.rawDependencies: Set 26 | get() { 27 | val configurationContainerField = DefaultDependencyHandler::class.java.getDeclaredField("configurationContainer") 28 | val configurationContainer = configurationContainerField.run { 29 | isAccessible = true 30 | get(dependencies) as ConfigurationContainer 31 | } 32 | val set = HashSet() 33 | configurationContainer.forEach { 34 | it.dependencies.forEach { dep -> 35 | set.add(dep as Dependency) 36 | } 37 | } 38 | return set 39 | } 40 | 41 | fun Project.projects(vararg names: String): List = run { 42 | listOf(*names).map { project(":$it") } 43 | } 44 | -------------------------------------------------------------------------------- /docs/changelog.md: -------------------------------------------------------------------------------- 1 | # 更新日志 2 | 3 | ## 2.1.1 4 | #### honoka-utils 1.1.2 5 | - 实现了`NewThreadFirstQueue`,并新增了`ThreadPoolUtils`,以创建先达到最大线程数再将任务放入阻塞队列中的线程池。 6 | - 优化了`ImageUtils`中关于文字换行的策略。 7 | - 优化了`FileUtils.getMainClasspath()`方法。 8 | - 新增了`LockUtils`,以在若干个对象上启用`synchronized`并执行代码块。 9 | 10 | #### honoka-kotlin-utils 1.1.1 11 | - 移除了`net.proxy`与`net.socket`包下除了`SocketUtils`以外的所有工具类。 12 | - 新增了`ScheduledTask`,以快速创建基于`ScheduledThreadPoolExecutor`的具有固定延迟(`scheduleWithFixedDelay`)的计划任务。 13 | - 为`Result`新增了一些有关日志记录的扩展方法。 14 | 15 | #### honoka-spring-boot-starter 1.0.2 16 | - 重构了`GlobalExceptionHandler`及其相关的方法。 17 | - 优化了`MybatisPlusConfig`中关于判断数据库类型的逻辑。 18 | - 新增了`QueryWrapper.updateChainWrapper()`。 19 | 20 | ## 2.1.0 21 | #### honoka-utils 1.1.1 22 | - 将code包重命名为basic。 23 | - 新增了一些文档注解(`@ThreadSafe`、`@NotThreadSafe`)。 24 | 25 | #### honoka-kotlin-utils 1.1.0 26 | - 将code包重命名为basic。 27 | - 实现了`ProxyPool`,可自动搜集、更新、检查多个来源的免费HTTPS代理。 28 | - 实现了`SocketForwarder`,可实现Socket连接中转,作为中间服务交换客户端与远程目标间传输的数据,支持在客户端连接时从多个远程目标服务中随机选择其中一个。 29 | - 实现了`NioSocketClient`,可快速实现基于非阻塞IO并支持发起多个连接的Socket客户端。 30 | - 实现了`ByteBufferIoStream`,可作为中间缓存数据流使用,既可向其中写入字节数据,也可从中读取指定数量的字节数据,同时也支持转换为`InputStream`或`OutputStream`(均由`ByteBufferIoStream`直接代理读写)供其他类使用。 31 | - 实现了`PropertyValueContainer`,用于支持在对象外部存储对象的某些属性值,通常用于在interface中定义可直接赋予初始值的属性。 32 | 33 | #### honoka-spring-boot-starter 1.0.1 34 | - 优化了security模块的一些实现。 35 | - 新增了mybatis模块。 36 | 37 | ## 2.0.0 38 | #### 工程 39 | - 移除honoka-framework-utils与honoka-json子项目。 40 | - 引入honoka-spring-boot-starter子项目,最低支持Java 17。 41 | - 需要引入Kotlin的项目统一采用Kotlin 1.8.10与Kotlin Coroutines 1.6.4版本。 42 | 43 | #### honoka-utils 1.1.0 44 | - 更改了一些类的包路径。 45 | - 移除了一些工具包依赖,使用hutool作为代替。 46 | - 适配hutool 5.8.25版本。 47 | 48 | #### honoka-kotlin-utils 1.0.1 49 | - 新增了一些扩展函数。 50 | - 实现`JsonWrapper`,用于快速根据JSON路径获取指定类型的值。 51 | 52 | #### honoka-spring-boot-starter 1.0.0 53 | - 初始版本,最低支持Spring Boot 3.2.5版本。 54 | - 迁移原honoka-framework-utils项目中的大部分功能作为core包,并新增了一些工具类。 55 | - 实现security包,用于快速实现基于JWT的单体应用的登录认证功能。 56 | 57 | ## 1.3.0 58 | #### 工程 59 | - 移除honoka-android-utils子项目,改造为独立的工程。 60 | 61 | #### honoka-utils 1.0.11 62 | - 调整工程依赖项配置,解决xml-apis库与pull-parser库的依赖冲突问题。 63 | 64 | #### honoka-kotlin-utils 1.0.0 65 | - 起始版本。包含`PartialAbstract`、`XmlUtils`,以及对hutool库与Collection的一些扩展方法。 66 | 67 | ## 1.2.2 68 | 原有子项目honoka-android-utils 1.0.2版本相关更新。 69 | 70 | ## 1.2.1 71 | 原有子项目honoka-android-utils 1.0.1版本相关更新。 72 | 73 | ## 1.2.0 74 | 原有子项目honoka-android-utils 1.0.0版本相关更新。 75 | 76 | ## 1.1.10 77 | #### honoka-utils 1.0.10 78 | - 移除`DefaultEnvironmentPathUtils`。 79 | - 优化`FileUtils`中的部分注释和方法定义。 80 | 81 | #### honoka-framework-utils 1.0.4 82 | - 添加`AbstractEmbeddedDatabaseUtils`,借助`AbstractEnvironmentPathUtils`快速拼接嵌入式数据库的JDBC URL。 83 | - 移除过时的Hibernate相关类。 84 | 85 | ## 1.1.9 86 | #### honoka-utils 1.0.9 87 | - `FileUtils` 88 | - 支持判断当前运行的Java应用程序是否位于JAR包中。 89 | - `getClasspath`方法更名为`getMainClasspath`,并优化了当Java应用程序在JAR包中被运行时获取主classpath的逻辑。 90 | - `AbstractEnvironmentPathUtils`、`DefaultEnvironmentPathUtils` 91 | - 某些应用可能会预先定义一些用于存放外部文件的基础路径,比如某些应用会自定义一个采用相对路径的工作目录,用于存放程序在运行时要输出的文件。应用在IDE中直接运行时,我们可能希望这个相对路径是`./[工程编译目录(如target或build)]/data/out`,而在JAR包中被运行时,我们可能希望这个路径是`./data/out`。可以通过继承`AbstractEnvironmentPathUtils`的方式来定义一些在不同环境下会返回不同路径的方法,来实现上述需求。 92 | 93 | ## 1.1.8 94 | #### honoka-framework-utils 1.0.3 95 | - 添加`ApiException`。 96 | 97 | ## 1.1.7 98 | #### honoka-utils 1.0.8 99 | - 为`ConsoleWindow`添加新的`init`方法,以实现在不创建托盘图标的情况下,能够执行退出动作。 100 | - 为`ConsoleWindowBuilder`添加一些意义更为明确的配置项。 101 | 102 | #### honoka-framework-utils 1.0.2 103 | - 添加`SpringBootConsoleWindow`类,可实现在Spring Boot应用启动前,配置、创建和显示`ConsoleWindow`,然后根据指定的主类启动Spring Boot应用,并管理相应的`ApplicationContext`。 104 | 105 | ## 1.1.6 106 | #### honoka-json-api 1.1.3 107 | - 为`JsonObject`与`JsonObjectService`增加新的`of(Object obj)`方法。 108 | - 为`JsonArray`与`JsonArrayService`增加新的`of(Collection collection, Class clazz)`方法。 109 | 110 | #### honoka-json-gson 1.1.3 111 | - 适配honoka-json-api 1.1.3。 112 | 113 | #### honoka-json-fastjson 1.1.3 114 | - 适配honoka-json-api 1.1.3。 115 | 116 | ## 1.1.5 117 | #### honoka-utils 1.0.7 118 | - 更新`CsvTable`API,增加泛型功能。 119 | - 增加`DefaultCsvTable`。 120 | - `ReflectUtils`增加`newInstance`方法。 121 | 122 | ## 1.1.4 123 | #### honoka-utils 1.0.6 124 | - 增强和优化`ColorAttributeSets`,现在可以缓存各个ANSI代码的`AttributeSet`和`Color`。 125 | - 增加`ColorfulOutputStream`,通过HTML格式保存控制台输出的彩色内容。 126 | - 优化`ConsoleOutputStream`。 127 | 128 | ## 1.1.3 129 | #### honoka-utils 1.0.5 130 | - 修改`ConsoleWindow`的初始化逻辑,移除公共构造器。 131 | - 修改`ConsoleWindow`部分字段的访问权限。 132 | - 创建`ConsoleWindowBuilder`以构建`ConsoleWindow`实例。 133 | - 移除`ConsoleWindow.setTrayIconMenuLocationOffset()`方法。 134 | 135 | ## 1.1.2 136 | #### honoka-utils 1.0.4 137 | - 添加`ThrowsConsumer`接口。 138 | 139 | #### honoka-framework-utils 1.0.1 140 | - 将模块依赖改为指定版本依赖。 141 | 142 | #### honoka-json-api 1.1.2 143 | - 将模块依赖改为指定版本依赖。 144 | 145 | #### honoka-json-gson 1.1.2 146 | - 将模块依赖改为指定版本依赖。 147 | 148 | #### honoka-json-fastjson 1.1.2 149 | - 将模块依赖改为指定版本依赖。 150 | 151 | ## 1.1.1 152 | #### honoka-utils 1.0.3 153 | - 移除`CodeUtils.doIgnoreExceptions()`,用`ActionUtils.doIgnoreException()`代替。 154 | - 移除`web`与`framework`包,将其独立到honoka-framework-utils模块中。 155 | 156 | #### honoka-framework-utils 1.0.0 157 | - 起始版本。 158 | 159 | #### honoka-json-api 1.1.1 160 | - 更新了`JsonObject`类中部分方法所使用的内部API。 161 | 162 | #### honoka-json-fastjson 1.1.1 163 | - 更新版本号。 164 | 165 | #### honoka-json-gson 1.1.1 166 | - 更新版本号。 167 | 168 | ## 1.1.0 169 | #### honoka-json-fastjson 1.1.0 170 | - 使用SPI重写对API的实现。 171 | - 实现公共配置类的回调接口,用户在通过公共配置类进行配置时,实现框架可收到回调。 172 | 173 | #### honoka-json-gson 1.1.0 174 | - 使用SPI重写对API的实现。 175 | - 实现公共配置类的回调接口,用户在通过公共配置类进行配置时,实现框架可收到回调。 176 | 177 | #### honoka-json-api 1.1.0 178 | - 使用SPI重写`JsonObject`、`JsonArray`等接口。 179 | - 添加了`JsonConfig`、`JsonConfigCallback`等公共配置类。 180 | 181 | ## 1.0.2 182 | #### honoka-utils 1.0.2 183 | - 优化`FileUtils`,去除Java 9+的API。 184 | - `FileUtils.checkResources()`更名为`copyResourceIfNotExists()`。 185 | - `FileUtils.copyResourceIfNotExists()`现在不会要求jar包内必须包含指定的资源。 186 | 187 | ## 1.0.1 188 | #### honoka-utils 1.0.1 189 | - 将原属于qqrobot-spring-boot-starter的`ImageUtils`类移到本库中。 190 | 191 | ## 1.0.0 192 | #### honoka-json-fastjson 1.0.0 193 | - 起始版本。 194 | 195 | #### honoka-json-gson 1.0.0 196 | - 起始版本。 197 | 198 | #### honoka-json-api 1.0.0 199 | - 起始版本。 200 | 201 | #### honoka-utils 1.0.0 202 | - 起始版本。 203 | -------------------------------------------------------------------------------- /docs/img/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kosaka-bun/honoka-sdk/7bc41d75805cae4f98d5e141b84d37f1dd22dd32/docs/img/1.png -------------------------------------------------------------------------------- /docs/img/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kosaka-bun/honoka-sdk/7bc41d75805cae4f98d5e141b84d37f1dd22dd32/docs/img/2.png -------------------------------------------------------------------------------- /docs/img/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kosaka-bun/honoka-sdk/7bc41d75805cae4f98d5e141b84d37f1dd22dd32/docs/img/3.png -------------------------------------------------------------------------------- /docs/img/badge/Java-17-brightgreen.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | Java: 17 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 19 | 21 | 25 | Java 26 | 30 | 17 31 | 32 | -------------------------------------------------------------------------------- /docs/img/badge/Java-8-brightgreen.svg: -------------------------------------------------------------------------------- 1 | Java: 8Java8 -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | ## For more details on how to configure your build environment visit 2 | # http://www.gradle.org/docs/current/userguide/build_environment.html 3 | # 4 | # Specifies the JVM arguments used for the daemon process. 5 | # The setting is particularly useful for tweaking memory settings. 6 | # Default value: -Xmx1024m -XX:MaxPermSize=256m 7 | org.gradle.jvmargs=-Xmx2048m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 8 | # 9 | # When configured, Gradle will run in incubating parallel mode. 10 | # This option should only be used with decoupled projects. More details, visit 11 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 12 | # org.gradle.parallel=true 13 | #Sun Feb 04 19:40:10 CST 2024 14 | android.enableJetifier=true 15 | android.useAndroidX=true 16 | # Kotlin code style for this project: "official" or "obsolete": 17 | kotlin.code.style=official -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | # project modules version 3 | root = "2.1.2-dev" 4 | honoka-utils = "1.1.3-dev" 5 | honoka-kotlin-utils = "1.1.2-dev" 6 | honoka-spring-boot-starter = "1.0.3-dev" 7 | 8 | # dependencies version 9 | spring-boot = "3.2.5" 10 | kotlin = "1.8.10" 11 | kotlin-coroutines = "1.6.4" 12 | lombok = "1.18.26" 13 | 14 | [plugins] 15 | kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } 16 | kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" } 17 | kotlin-lombok = { id = "org.jetbrains.kotlin.plugin.lombok", version.ref = "kotlin" } 18 | kotlin-spring = { id = "org.jetbrains.kotlin.plugin.spring", version.ref = "kotlin" } 19 | dependency-management = "io.spring.dependency-management:1.1.6" 20 | 21 | [libraries] 22 | spring-boot-dependencies = { module = "org.springframework.boot:spring-boot-dependencies", version.ref = "spring-boot" } 23 | kotlin-bom = { module = "org.jetbrains.kotlin:kotlin-bom", version.ref = "kotlin" } 24 | slf4j-api = "org.slf4j:slf4j-api:2.0.7" 25 | logback = "ch.qos.logback:logback-classic:1.4.14" 26 | lombok = { module = "org.projectlombok:lombok", version.ref = "lombok" } 27 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kosaka-bun/honoka-sdk/7bc41d75805cae4f98d5e141b84d37f1dd22dd32/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /honoka-kotlin-utils/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import de.honoka.gradle.buildsrc.MavenPublish.setupVersionAndPublishing 2 | import de.honoka.gradle.buildsrc.implementationApi 3 | 4 | setupVersionAndPublishing(libs.versions.honoka.kotlin.utils.get()) 5 | 6 | dependencies { 7 | implementationApi("de.honoka.sdk:honoka-utils:1.1.3-dev") 8 | implementation(libs.logback) 9 | } 10 | 11 | tasks { 12 | compileKotlin { 13 | dependsOn(":honoka-utils:publish") 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /honoka-kotlin-utils/src/main/java/de/honoka/sdk/util/kotlin/basic/BooleanBuilder.kt: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.util.kotlin.basic 2 | 3 | /* 4 | * 注意:使用该类进行逻辑运算时,会影响逻辑运算的短路规则,仅适用于对短路规则没有硬性要求的运算。 5 | */ 6 | class BooleanBuilder(private var boolean: Boolean = false) { 7 | 8 | companion object { 9 | 10 | inline fun calc(block: BooleanBuilder.() -> Boolean): Boolean = BooleanBuilder().run(block) 11 | } 12 | 13 | private fun Boolean.save(): Boolean = also { 14 | boolean = it 15 | } 16 | 17 | fun init(value: Boolean): Boolean = value.save() 18 | 19 | fun and(value: Boolean): Boolean = (boolean && value).save() 20 | 21 | fun or(value: Boolean): Boolean = (boolean || value).save() 22 | } 23 | -------------------------------------------------------------------------------- /honoka-kotlin-utils/src/main/java/de/honoka/sdk/util/kotlin/basic/Collections.kt: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.util.kotlin.basic 2 | 3 | import java.util.concurrent.ConcurrentMap 4 | 5 | data class MapEntryImpl(override val key: K, override val value: V) : Map.Entry 6 | 7 | inline fun MutableIterable.iterate(block: MutableIterator.(T) -> Unit) { 8 | iterator().run { 9 | while(hasNext()) { 10 | block(next()) 11 | } 12 | } 13 | } 14 | 15 | inline fun MutableMap.iterate( 16 | block: MutableIterator>.(MutableMap.MutableEntry) -> Unit 17 | ) = entries.iterate(block) 18 | 19 | inline fun MutableMap.removeIf(block: (Map.Entry) -> Boolean) { 20 | iterate { 21 | if(block(it)) remove() 22 | } 23 | } 24 | 25 | inline fun Iterable.forEachRun(action: T.() -> Unit) { 26 | forEach { 27 | it.action() 28 | } 29 | } 30 | 31 | inline fun Iterable.forEachCatching(action: (T) -> Unit) { 32 | forEach { 33 | runCatching { 34 | action(it) 35 | } 36 | } 37 | } 38 | 39 | inline fun Map.forEachCatching(action: (Map.Entry) -> Unit) { 40 | forEach { 41 | runCatching { 42 | action(it) 43 | } 44 | } 45 | } 46 | 47 | inline fun Iterable.forEachCatchingRun(action: T.() -> Unit) { 48 | forEachCatching { 49 | it.action() 50 | } 51 | } 52 | 53 | /* 54 | * ConcurrentMap的forEach比较特殊,它可以在遍历过程中即时发现被修改或移除的键值对。 55 | * 若某个键值对在forEach遍历到它之前就被移除了,此方法在后续的遍历中不会再遍历它。 56 | * 57 | * IDEA并未对ConcurrentMap的特殊性进行处理,尝试调用ConcurrentMap的原生forEach方法 58 | * 时会被建议替换为Kotlin的forEach扩展函数。 59 | */ 60 | @Suppress("JavaMapForEach") 61 | inline fun ConcurrentMap.forEachInstant(crossinline action: (Map.Entry) -> Unit) { 62 | forEach { k, v -> 63 | action(MapEntryImpl(k, v)) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /honoka-kotlin-utils/src/main/java/de/honoka/sdk/util/kotlin/basic/Exceptions.kt: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.util.kotlin.basic 2 | 3 | import kotlin.reflect.KClass 4 | 5 | fun exception(message: String? = null): Nothing = throw RuntimeException(message) 6 | 7 | fun Throwable?.isAnyType(vararg types: KClass): Boolean { 8 | this ?: return false 9 | return this::class.isSubClassOfAny(*types) 10 | } 11 | 12 | fun Throwable?.isAnyType(types: Collection>): Boolean = isAnyType(*types.toTypedArray()) 13 | -------------------------------------------------------------------------------- /honoka-kotlin-utils/src/main/java/de/honoka/sdk/util/kotlin/basic/Logger.kt: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.util.kotlin.basic 2 | 3 | import ch.qos.logback.classic.Level 4 | import org.slf4j.Logger 5 | import org.slf4j.LoggerFactory 6 | import java.util.concurrent.ConcurrentHashMap 7 | import kotlin.reflect.KClass 8 | 9 | private val loggerCache = ConcurrentHashMap, Logger>() 10 | 11 | val KClass<*>.log: Logger 12 | get() = loggerCache[this] ?: run { 13 | var clazz = java 14 | if(clazz.simpleName.lowercase().contains("\$\$springcglib")) { 15 | clazz = java.superclass ?: clazz 16 | } 17 | LoggerFactory.getLogger(clazz).also { 18 | loggerCache[this] = it 19 | } 20 | } 21 | 22 | val Any.log: Logger 23 | get() = this::class.log 24 | 25 | fun Logger.off() { 26 | when(this) { 27 | is ch.qos.logback.classic.Logger -> level = Level.OFF 28 | else -> throw UnsupportedOperationException() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /honoka-kotlin-utils/src/main/java/de/honoka/sdk/util/kotlin/basic/PartialAbstract.kt: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.util.kotlin.basic 2 | 3 | /** 4 | * “部分抽象”设计模式。 5 | * 6 | * 扩展函数仅当其被定义在包中或单例对象中时,才可以在别处被导入使用。若一个单例对象中包含需要不同依赖者 7 | * 各自进行实现的部分,又包含与该对象高度相关的扩展函数,则可以实现此抽象类以引入一个“部分抽象”结构。 8 | * 9 | * 在上述情况中,若直接将上述单例对象改为抽象类,则在依赖者自定义单例对象实现这一抽象类之前,类中的所有 10 | * 扩展函数都将无法在其他类中被直接调用,即便在其他类定义一个类型为该抽象类的属性,也依旧如此。 11 | */ 12 | @Suppress("MemberVisibilityCanBePrivate") 13 | abstract class PartialAbstract { 14 | 15 | lateinit var abstractPart: T 16 | 17 | fun initAbstractPart(abstractPart: T) { 18 | this.abstractPart = abstractPart 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /honoka-kotlin-utils/src/main/java/de/honoka/sdk/util/kotlin/basic/PropertyValueContainer.kt: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.util.kotlin.basic 2 | 3 | import de.honoka.sdk.util.basic.javadoc.ThreadSafe 4 | import java.lang.ref.WeakReference 5 | import java.util.* 6 | import java.util.concurrent.ConcurrentHashMap 7 | import java.util.concurrent.LinkedBlockingQueue 8 | import java.util.concurrent.ThreadPoolExecutor 9 | import java.util.concurrent.ThreadPoolExecutor.DiscardPolicy 10 | import java.util.concurrent.TimeUnit 11 | import kotlin.jvm.internal.CallableReference 12 | import kotlin.reflect.KProperty 13 | 14 | /** 15 | * 采用对象的弱引用作为key的对象属性值容器,用于支持在interface中直接定义可赋初始值的属性。 16 | * 17 | * 注意:对象的属性在使用本类存取属性值时会有一定性能问题,速度在理论上远不如直接使用对象中的字段来 18 | * 进行属性值存取的属性。 19 | */ 20 | @ThreadSafe 21 | @Suppress("UNCHECKED_CAST", "MemberVisibilityCanBePrivate") 22 | object PropertyValueContainer { 23 | 24 | private class KPropertyReference(property: KProperty<*>) { 25 | 26 | val ref = WeakReference(property.cast().boundReceiver) 27 | 28 | val signature: String = property.toString() 29 | 30 | val targetHashCode: Int = Objects.hash(ref.get(), signature) 31 | 32 | override fun equals(other: Any?): Boolean { 33 | if(other !is KPropertyReference) return false 34 | val target = ref.get() ?: return this === other 35 | val result = BooleanBuilder.calc { 36 | init(target === other.ref.get()) 37 | and(signature == other.signature) 38 | and(targetHashCode == other.targetHashCode) 39 | } 40 | return result 41 | } 42 | 43 | override fun hashCode(): Int = targetHashCode 44 | } 45 | 46 | private val map = ConcurrentHashMap() 47 | 48 | private val nullValue = Any() 49 | 50 | private val executor by lazy { 51 | ThreadPoolExecutor( 52 | 1, 1, 0, TimeUnit.MILLISECONDS, 53 | executorQueue, DiscardPolicy() 54 | ) 55 | } 56 | 57 | private val executorQueue = LinkedBlockingQueue(1) 58 | 59 | @Volatile 60 | private var lastCleanTime = 0L 61 | 62 | fun get(property: KProperty<*>): T = getOrNull(property)!! 63 | 64 | fun getOrNull(property: KProperty<*>): T? { 65 | val result = map[KPropertyReference(property)] 66 | clean() 67 | return if(result === nullValue) null else result as T? 68 | } 69 | 70 | fun getOrInit(property: KProperty<*>, initialValue: T): T = run { 71 | getOrInit(property, initialValue as T?)!! 72 | } 73 | 74 | @JvmName("getOrInitNullable") 75 | fun getOrInit(property: KProperty<*>, initialValue: T?): T? { 76 | try { 77 | property as CallableReference 78 | val ref = KPropertyReference(property) 79 | map[ref]?.let { 80 | return if(it === nullValue) null else it as T 81 | } 82 | synchronized(property.boundReceiver) { 83 | map[ref]?.let { 84 | return if(it === nullValue) null else it as T 85 | } 86 | map[ref] = initialValue ?: nullValue 87 | return initialValue 88 | } 89 | } finally { 90 | clean() 91 | } 92 | } 93 | 94 | fun set(property: KProperty<*>, value: Any?) { 95 | map[KPropertyReference(property)] = value ?: nullValue 96 | clean() 97 | } 98 | 99 | private fun clean() { 100 | val time = System.currentTimeMillis() 101 | if(time - lastCleanTime < 1000) return 102 | if(executorQueue.isNotEmpty()) return 103 | lastCleanTime = time 104 | executor.submit { 105 | map.removeIf { (k) -> k.ref.get() == null } 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /honoka-kotlin-utils/src/main/java/de/honoka/sdk/util/kotlin/basic/Various.kt: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.util.kotlin.basic 2 | 3 | import de.honoka.sdk.util.basic.CodeUtils 4 | import org.slf4j.event.Level 5 | import java.util.* 6 | import kotlin.reflect.KClass 7 | 8 | fun KClass<*>.isSubClassOf(clazz: KClass): Boolean = clazz.java.isAssignableFrom(java) 9 | 10 | fun KClass<*>.isSubClassOfAny(vararg classes: KClass): Boolean { 11 | classes.forEach { 12 | if(this.isSubClassOf(it)) return true 13 | } 14 | return false 15 | } 16 | 17 | @Suppress("UNCHECKED_CAST", "NOTHING_TO_INLINE") 18 | inline fun Any?.cast(): T = this as T 19 | 20 | inline fun tryBlockNullable( 21 | times: Int, 22 | throwOnExceedTimes: Boolean = true, 23 | exceptionTypesToIgnore: List> = listOf(Throwable::class), 24 | block: (Int) -> T? 25 | ): T? { 26 | var throwable: Throwable? = null 27 | repeat(times) { i -> 28 | try { 29 | return block(i) 30 | } catch(t: Throwable) { 31 | //若不是应当忽略的异常类型 32 | exceptionTypesToIgnore.firstOrNull { t::class.isSubClassOf(it) } ?: throw t 33 | throwable = t 34 | } 35 | } 36 | if(throwOnExceedTimes) throw throwable!! 37 | return null 38 | } 39 | 40 | inline fun tryBlock( 41 | times: Int, 42 | throwOnExceedTimes: Boolean = true, 43 | exceptionTypesToIgnore: List> = listOf(Throwable::class), 44 | block: (Int) -> T 45 | ): T = tryBlockNullable(times, throwOnExceedTimes, exceptionTypesToIgnore, block)!! 46 | 47 | inline fun repeatCatching(times: Int, block: (Int) -> Unit) { 48 | repeat(times) { 49 | runCatching { block(times) } 50 | } 51 | } 52 | 53 | val Date.weekdayNum: Int 54 | get() = Calendar.getInstance().run { 55 | setTime(this@weekdayNum) 56 | get(Calendar.DAY_OF_WEEK).let { 57 | if(it != Calendar.SUNDAY) it - 1 else 7 58 | } 59 | } 60 | 61 | fun Result.printStackIfFailed() { 62 | if(isSuccess) return 63 | exceptionOrNull()?.printStackTrace() 64 | } 65 | 66 | fun Result.logIfFailed(level: Level = Level.ERROR, msg: String = "") { 67 | if(isSuccess) return 68 | val log = CodeUtils.getCallerClass().kotlin.log 69 | val throwable = exceptionOrNull() ?: return 70 | when(level) { 71 | Level.ERROR -> log.error(msg, throwable) 72 | Level.INFO -> log.info(msg, throwable) 73 | Level.WARN -> log.warn(msg, throwable) 74 | Level.DEBUG -> log.debug(msg, throwable) 75 | Level.TRACE -> log.trace(msg, throwable) 76 | else -> log.error(msg, throwable) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /honoka-kotlin-utils/src/main/java/de/honoka/sdk/util/kotlin/concurrent/ScheduledTask.kt: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.util.kotlin.concurrent 2 | 3 | import java.io.Closeable 4 | import java.util.concurrent.ScheduledFuture 5 | import java.util.concurrent.ScheduledThreadPoolExecutor 6 | import java.util.concurrent.TimeUnit 7 | import kotlin.time.Duration 8 | 9 | class ScheduledTask( 10 | private val delay: String, 11 | private val initialDelay: String = "0s", 12 | private val action: () -> Unit 13 | ) : Closeable { 14 | 15 | private var executor: ScheduledThreadPoolExecutor? = null 16 | 17 | private var runningTask: ScheduledFuture<*>? = null 18 | 19 | var exceptionCallback: (Throwable) -> Unit = {} 20 | 21 | @Synchronized 22 | fun startup() { 23 | close() 24 | executor = ThreadPoolUtilsExt.newScheduledPool(1) 25 | val realAction: () -> Unit = { 26 | runCatching { 27 | action() 28 | }.getOrElse { 29 | exceptionCallback(it) 30 | } 31 | } 32 | val delay = Duration.parse(delay).inWholeMilliseconds 33 | val initialDelay = Duration.parse(initialDelay).inWholeMilliseconds 34 | runningTask = executor!!.scheduleWithFixedDelay( 35 | realAction, initialDelay, delay, TimeUnit.MILLISECONDS 36 | ) 37 | } 38 | 39 | @Synchronized 40 | override fun close() { 41 | runningTask?.cancel(true) 42 | executor?.shutdownNowAndWait() 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /honoka-kotlin-utils/src/main/java/de/honoka/sdk/util/kotlin/concurrent/Thread.kt: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.util.kotlin.concurrent 2 | 3 | import de.honoka.sdk.util.concurrent.LockUtils 4 | import java.util.concurrent.ExecutorService 5 | import java.util.concurrent.Future 6 | import java.util.concurrent.TimeUnit 7 | 8 | /** 9 | * 将对象存入ThreadLocal中,然后执行一段代码块,执行完成后清除ThreadLocal中的对象, 10 | * 最后返回代码块的返回值。 11 | * 用于在线程池中的线程使用ThreadLocal时,防止未正确释放ThreadLocal中的内容。 12 | */ 13 | inline fun ThreadLocal.use(t: T, block: () -> Any?): Any? { 14 | try { 15 | set(t) 16 | return block() 17 | } finally { 18 | remove() 19 | } 20 | } 21 | 22 | fun Future.getOrCancel(timeout: Long, unit: TimeUnit): T { 23 | try { 24 | return get(timeout, unit) 25 | } catch(t: Throwable) { 26 | cancel(true) 27 | throw t 28 | } 29 | } 30 | 31 | fun ExecutorService.shutdownNowAndWait( 32 | timeout: Long = Long.MAX_VALUE, 33 | unit: TimeUnit = TimeUnit.SECONDS, 34 | ignoreIfAlreadyShutdown: Boolean = true 35 | ): List = run { 36 | if(isShutdown && ignoreIfAlreadyShutdown) return@run listOf() 37 | shutdownNow().apply { awaitTermination(timeout, unit) } 38 | } 39 | 40 | inline fun synchronized2(lock1: Any, lock2: Any, block: () -> T): T = run { 41 | synchronized(lock1) { 42 | synchronized(lock2, block) 43 | } 44 | } 45 | 46 | inline fun synchronized3(lock1: Any, lock2: Any, lock3: Any, block: () -> T): T = run { 47 | synchronized(lock1) { 48 | synchronized(lock2) { 49 | synchronized(lock3, block) 50 | } 51 | } 52 | } 53 | 54 | fun synchronizedItems(vararg items: Any, block: () -> T): T = run { 55 | LockUtils.synchronizedItems(items.asIterable(), block) 56 | } 57 | -------------------------------------------------------------------------------- /honoka-kotlin-utils/src/main/java/de/honoka/sdk/util/kotlin/concurrent/ThreadPoolUtilsExt.kt: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.util.kotlin.concurrent 2 | 3 | import de.honoka.sdk.util.concurrent.ThreadPoolUtils 4 | import java.util.concurrent.RejectedExecutionHandler 5 | import java.util.concurrent.ScheduledThreadPoolExecutor 6 | import java.util.concurrent.ThreadPoolExecutor.AbortPolicy 7 | 8 | object ThreadPoolUtilsExt { 9 | 10 | fun newScheduledPool( 11 | coreSize: Int, rejectedExecutionHandler: RejectedExecutionHandler = AbortPolicy() 12 | ): ScheduledThreadPoolExecutor = run { 13 | ThreadPoolUtils.newScheduledPool(coreSize, rejectedExecutionHandler) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /honoka-kotlin-utils/src/main/java/de/honoka/sdk/util/kotlin/io/ByteBufferIoStream.kt: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.util.kotlin.io 2 | 3 | import de.honoka.sdk.util.basic.javadoc.NotThreadSafe 4 | 5 | @NotThreadSafe 6 | class ByteBufferIoStream : MiddleIoStream() { 7 | 8 | private val buffer = ArrayList() 9 | 10 | @Volatile 11 | private var readPointer = 0 12 | 13 | private fun privateRead(): Byte? { 14 | if(isEmpty()) return null 15 | readPointer++ 16 | return buffer[readPointer - 1] 17 | } 18 | 19 | override fun read(): Int = privateRead()?.toUByte()?.toInt() ?: -1 20 | 21 | override fun read(b: ByteArray, off: Int, len: Int): Int { 22 | if(len < 1) return 0 23 | if(off + len > b.size) { 24 | throw IndexOutOfBoundsException("size: ${b.size}, off: $off, len: $len") 25 | } 26 | var count = 0 27 | while(count < len) { 28 | val aByte = privateRead() ?: break 29 | b[off + count] = aByte 30 | count++ 31 | if(isEmpty()) break 32 | } 33 | if(count < 1) return -1 34 | buffer.run { 35 | subList(readPointer, size).toTypedArray().let { 36 | clear() 37 | addAll(it) 38 | } 39 | } 40 | readPointer = 0 41 | return count 42 | } 43 | 44 | override fun write(b: Int) { 45 | buffer.add(b.toByte()) 46 | } 47 | 48 | override fun write(b: ByteArray, off: Int, len: Int) { 49 | if(len < 1) return 50 | if(off < 0 || off + len > b.size) { 51 | throw IndexOutOfBoundsException("size: ${b.size}, off: $off, len: $len") 52 | } 53 | val subArray = if(off == 0 && len == b.size) b else b.copyOfRange(off, off + len) 54 | buffer.addAll(subArray.asList()) 55 | } 56 | 57 | override fun available(): Int = buffer.size - readPointer 58 | } 59 | -------------------------------------------------------------------------------- /honoka-kotlin-utils/src/main/java/de/honoka/sdk/util/kotlin/io/MiddleIoStream.kt: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.util.kotlin.io 2 | 3 | import de.honoka.sdk.util.basic.javadoc.NotThreadSafe 4 | import de.honoka.sdk.util.basic.javadoc.ThreadSafe 5 | import java.io.Closeable 6 | import java.io.InputStream 7 | import java.io.OutputStream 8 | 9 | @NotThreadSafe 10 | abstract class MiddleIoStream : Closeable { 11 | 12 | @NotThreadSafe 13 | private inner class In : InputStream() { 14 | 15 | override fun read(): Int = this@MiddleIoStream.read() 16 | 17 | override fun read(b: ByteArray, off: Int, len: Int): Int = this@MiddleIoStream.read(b, off, len) 18 | 19 | fun superRead(b: ByteArray, off: Int, len: Int): Int = super.read(b, off, len) 20 | 21 | override fun available(): Int = this@MiddleIoStream.available() 22 | 23 | override fun close() { 24 | this@MiddleIoStream.doClose() 25 | } 26 | } 27 | 28 | @NotThreadSafe 29 | private inner class Out : OutputStream() { 30 | 31 | override fun write(b: Int) { 32 | this@MiddleIoStream.write(b) 33 | } 34 | 35 | override fun write(b: ByteArray, off: Int, len: Int) { 36 | this@MiddleIoStream.write(b, off, len) 37 | } 38 | 39 | fun superWrite(b: ByteArray, off: Int, len: Int) { 40 | super.write(b, off, len) 41 | } 42 | 43 | override fun flush() { 44 | this@MiddleIoStream.flush() 45 | } 46 | 47 | override fun close() { 48 | this@MiddleIoStream.doClose() 49 | } 50 | } 51 | 52 | private val inputStream = In() 53 | 54 | private val outputStream = Out() 55 | 56 | @Volatile 57 | private var closed: Boolean = false 58 | 59 | fun asIn(): InputStream = inputStream 60 | 61 | fun asOut(): OutputStream = outputStream 62 | 63 | protected abstract fun read(): Int 64 | 65 | protected open fun read(b: ByteArray, off: Int, len: Int): Int = inputStream.superRead(b, off, len) 66 | 67 | open fun read(limit: Int = Int.MAX_VALUE): ByteArray { 68 | val count = available().let { 69 | if(limit > it) it else limit 70 | } 71 | val buffer = ByteArray(count) 72 | inputStream.read(buffer) 73 | return buffer 74 | } 75 | 76 | @Synchronized 77 | fun lockAndRead(limit: Int = Int.MAX_VALUE): ByteArray = read(limit) 78 | 79 | protected abstract fun write(b: Int) 80 | 81 | protected open fun write(b: ByteArray, off: Int, len: Int) { 82 | outputStream.superWrite(b, off, len) 83 | } 84 | 85 | open fun write(b: ByteArray) { 86 | outputStream.write(b) 87 | } 88 | 89 | @Synchronized 90 | fun lockAndWrite(b: ByteArray) { 91 | write(b) 92 | } 93 | 94 | abstract fun available(): Int 95 | 96 | fun isEmpty(): Boolean = available() < 1 97 | 98 | open fun flush() {} 99 | 100 | @ThreadSafe 101 | private fun doClose() { 102 | synchronized(this) { 103 | if(closed) return 104 | closed = true 105 | } 106 | close() 107 | } 108 | 109 | override fun close() {} 110 | } 111 | -------------------------------------------------------------------------------- /honoka-kotlin-utils/src/main/java/de/honoka/sdk/util/kotlin/net/http/Http.kt: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.util.kotlin.net.http 2 | 3 | import cn.hutool.http.HttpRequest 4 | import cn.hutool.json.JSONObject 5 | import de.honoka.sdk.util.kotlin.text.toJsonObject 6 | import java.util.concurrent.ConcurrentHashMap 7 | import kotlin.reflect.jvm.javaField 8 | 9 | internal val browserHeadersCache = ConcurrentHashMap() 10 | 11 | fun HttpRequest.params(params: Map) { 12 | val query = StringBuilder() 13 | params.entries.forEachIndexed { i, it -> 14 | query.append("${it.key}=${it.value}") 15 | if(i < params.size - 1) query.append("&") 16 | } 17 | body(query.toString()) 18 | } 19 | 20 | internal fun HttpRequest.browserHeaders(headersFileName: String) { 21 | val json = browserHeadersCache[headersFileName] ?: run { 22 | val path = "/http/static-headers/${headersFileName}.json" 23 | val declaringClass = ::browserHeadersCache.javaField!!.declaringClass 24 | declaringClass.getResource(path)!!.readText().toJsonObject() 25 | } 26 | json.forEach { k, v -> 27 | header(k, v?.toString() ?: "", true) 28 | } 29 | } 30 | 31 | fun HttpRequest.browserHeaders() { 32 | browserHeaders("document") 33 | } 34 | 35 | fun HttpRequest.browserApiHeaders() { 36 | browserHeaders("api") 37 | } 38 | 39 | fun HttpRequest.setHttpProxy(proxy: String) { 40 | val parts = proxy.split(":") 41 | setHttpProxy(parts[0], parts[1].toInt()) 42 | } 43 | -------------------------------------------------------------------------------- /honoka-kotlin-utils/src/main/java/de/honoka/sdk/util/kotlin/net/http/HttpUtilExt.kt: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.util.kotlin.net.http 2 | 3 | import cn.hutool.http.HttpUtil 4 | 5 | @Suppress("MemberVisibilityCanBePrivate") 6 | object HttpUtilExt { 7 | 8 | fun getWithBrowserHeaders( 9 | url: String, 10 | timeout: Int? = null, 11 | useApiHeaders: Boolean = false 12 | ): String { 13 | val request = HttpUtil.createGet(url).apply { 14 | if(useApiHeaders) { 15 | browserApiHeaders() 16 | } else { 17 | browserHeaders() 18 | } 19 | timeout?.let { timeout(it) } 20 | } 21 | return request.execute().body() 22 | } 23 | 24 | fun getWithBrowserApiHeaders( 25 | url: String, 26 | timeout: Int? = null 27 | ): String = getWithBrowserHeaders(url, timeout, true) 28 | } 29 | -------------------------------------------------------------------------------- /honoka-kotlin-utils/src/main/java/de/honoka/sdk/util/kotlin/net/socket/SocketUtils.kt: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.util.kotlin.net.socket 2 | 3 | import de.honoka.sdk.util.kotlin.basic.cast 4 | import de.honoka.sdk.util.kotlin.basic.exception 5 | import de.honoka.sdk.util.kotlin.basic.tryBlock 6 | import java.net.BindException 7 | import java.net.InetSocketAddress 8 | import java.nio.channels.ServerSocketChannel 9 | 10 | @Suppress("MemberVisibilityCanBePrivate") 11 | object SocketUtils { 12 | 13 | fun newServerSocketChannel(firstTryPort: Int, tryCount: Int = 1): ServerSocketChannel { 14 | val channel = ServerSocketChannel.open() 15 | channel.run { 16 | configureBlocking(false) 17 | runCatching { 18 | tryBlock(tryCount, exceptionTypesToIgnore = listOf(BindException::class)) { 19 | bind(InetSocketAddress(firstTryPort + it)) 20 | } 21 | }.getOrElse { 22 | close() 23 | throw it 24 | } 25 | } 26 | return channel 27 | } 28 | 29 | fun findAvailablePort(firstTryPort: Int, tryCount: Int = 1): Int { 30 | newServerSocketChannel(firstTryPort, tryCount).use { 31 | return it.localAddress.cast().port 32 | } 33 | } 34 | 35 | fun parseHostAndPort(address: String): Pair { 36 | val parts = address.split(":") 37 | runCatching { 38 | return parts[0] to parts[1].toInt() 39 | }.getOrElse { 40 | exception("Invalid address: $address") 41 | } 42 | } 43 | 44 | fun parseInetSocketAddress(address: String): InetSocketAddress = parseHostAndPort(address).run { 45 | InetSocketAddress(first, second) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /honoka-kotlin-utils/src/main/java/de/honoka/sdk/util/kotlin/text/Json.kt: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.util.kotlin.text 2 | 3 | import cn.hutool.json.JSON 4 | import cn.hutool.json.JSONArray 5 | import cn.hutool.json.JSONObject 6 | import cn.hutool.json.JSONUtil 7 | 8 | fun String.toJsonObject(): JSONObject = JSONUtil.parseObj(this) 9 | 10 | fun String.toJsonArray(): JSONArray = JSONUtil.parseArray(this) 11 | 12 | fun JSON.wrapper(): JsonWrapper = JsonWrapper(this) 13 | 14 | fun String.toJsonWrapper(): JsonWrapper = JSONUtil.parse(this).wrapper() 15 | 16 | fun Any?.toJsonString(pretty: Boolean = false): String = run { 17 | if(pretty) { 18 | JSONUtil.toJsonPrettyStr(this) 19 | } else { 20 | JSONUtil.toJsonStr(this) 21 | } 22 | } 23 | 24 | inline fun JSONArray.forEachWrapper(block: (JsonWrapper) -> Unit) { 25 | forEach { block((it as JSON).wrapper()) } 26 | } 27 | -------------------------------------------------------------------------------- /honoka-kotlin-utils/src/main/java/de/honoka/sdk/util/kotlin/text/JsonWrapper.kt: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.util.kotlin.text 2 | 3 | import cn.hutool.json.JSON 4 | import cn.hutool.json.JSONArray 5 | import cn.hutool.json.JSONObject 6 | 7 | @Suppress("MemberVisibilityCanBePrivate") 8 | class JsonWrapper internal constructor(private val json: JSON) { 9 | 10 | operator fun get(path: String): JSONObject = getObj(path) 11 | 12 | //not null 13 | 14 | fun getObj(path: String): JSONObject = getObjOrNull(path)!! 15 | 16 | fun getArray(path: String): JSONArray = getArrayOrNull(path)!! 17 | 18 | inline fun getBean(path: String): T = getBeanOrNull(path)!! 19 | 20 | inline fun getList(path: String): List = getListOrNull(path)!! 21 | 22 | fun getStr(path: String): String = getStrOrNull(path)!! 23 | 24 | fun getInt(path: String): Int = getIntOrNull(path)!! 25 | 26 | fun getLong(path: String): Long = getLongOrNull(path)!! 27 | 28 | fun getBool(path: String): Boolean = getBoolOrNull(path)!! 29 | 30 | fun getDouble(path: String): Double = getDoubleOrNull(path)!! 31 | 32 | //nullable 33 | 34 | fun getObjOrNull(path: String): JSONObject? = json.getByPath(path) as JSONObject? 35 | 36 | fun getArrayOrNull(path: String): JSONArray? = json.getByPath(path) as JSONArray? 37 | 38 | inline fun getBeanOrNull(path: String): T? = getObjOrNull(path)?.toBean(T::class.java) 39 | 40 | inline fun getListOrNull(path: String): List? = getArrayOrNull(path)?.toList(T::class.java) 41 | 42 | fun getStrOrNull(path: String): String? = json.getByPath(path)?.toString() 43 | 44 | fun getIntOrNull(path: String): Int? = (json.getByPath(path) as Number?)?.toInt() 45 | 46 | fun getLongOrNull(path: String): Long? = (json.getByPath(path) as Number?)?.toLong() 47 | 48 | fun getBoolOrNull(path: String): Boolean? = json.getByPath(path) as Boolean? 49 | 50 | fun getDoubleOrNull(path: String): Double? = (json.getByPath(path) as Number?)?.toDouble() 51 | 52 | override fun toString(): String = json.toString() 53 | } 54 | -------------------------------------------------------------------------------- /honoka-kotlin-utils/src/main/java/de/honoka/sdk/util/kotlin/text/StringWrapper.kt: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.util.kotlin.text 2 | 3 | class StringWrapper(internal var str: StringBuilder) { 4 | 5 | fun append(vararg s: Any?) { 6 | str.append(*s) 7 | } 8 | 9 | fun replace(oldValue: String, newValue: String, ignoreCase: Boolean = false) { 10 | str = str.toString().replace(oldValue, newValue, ignoreCase).toStringBuilder() 11 | } 12 | 13 | fun replace(regex: Regex, replacement: String) { 14 | str = str.replace(regex, replacement).toStringBuilder() 15 | } 16 | 17 | fun trim() { 18 | str = str.trim().toString().toStringBuilder() 19 | } 20 | } 21 | 22 | fun String?.process(block: StringWrapper.() -> Unit): String { 23 | val wrapper = StringWrapper(this?.toStringBuilder() ?: StringBuilder()) 24 | wrapper.block() 25 | return wrapper.str.toString() 26 | } 27 | -------------------------------------------------------------------------------- /honoka-kotlin-utils/src/main/java/de/honoka/sdk/util/kotlin/text/Strings.kt: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.util.kotlin.text 2 | 3 | import org.intellij.lang.annotations.Language 4 | 5 | /** 6 | * 将多行字符串拼接为单行,支持行边界字符,默认使用“|”字符作为行边界。 7 | * 8 | * 基本逻辑: 9 | * 1. 去掉传入字符串两端的空格。 10 | * 2. 将字符串拆分为行。 11 | * 3. 遍历每行,先去掉每行两端的空格。 12 | * 4. 若某行以“|”开头,则去掉第一个“|”。以“|”结尾,则去掉最后一个“|”。 13 | * 5. 将处理好的行拼接到`builder`中。 14 | * 6. 返回`builder`的值。 15 | * 16 | * 注意事项: 17 | * 1. 本方法仅适合用于Kotlin代码中的多行文本字面量。 18 | * 2. 若某行以`${}`开头或结尾,由于不知道表达式最终的值是否会以“|”开头或结尾,为了避免去除 19 | * 表达式结果当中包含的“|”,请在以`${}`开头或结尾的行的行首或行尾额外添加一个“|”。 20 | */ 21 | fun String.singleLine(borderChar: Char = '|', whiteSpaceOnEnd: Boolean = false): String { 22 | val builder = StringBuilder() 23 | trim().lineSequence().forEach { 24 | val line = it.trim() 25 | val startIndex = if(line.startsWith(borderChar)) 1 else 0 26 | val endIndex = if(line.endsWith(borderChar)) line.lastIndex else line.length 27 | if(startIndex >= endIndex) return@forEach 28 | builder.run { 29 | append(line.substring(startIndex, endIndex)) 30 | if(whiteSpaceOnEnd) append(" ") 31 | } 32 | } 33 | return builder.toString().trim() 34 | } 35 | 36 | /** 37 | * 直接将多行字符串的每行去除两端空格后,拼接在一起。 38 | */ 39 | fun String.simpleSingleLine(whiteSpaceOnEnd: Boolean = false): String { 40 | val builder = StringBuilder() 41 | trim().lineSequence().forEach { 42 | builder.run { 43 | append(it.trim()) 44 | if(whiteSpaceOnEnd) append(" ") 45 | } 46 | } 47 | return builder.toString() 48 | } 49 | 50 | fun String.toStringBuilder(): StringBuilder = StringBuilder(this) 51 | 52 | fun String?.find(@Language("RegExp") regex: String): List { 53 | this ?: return listOf() 54 | return Regex(regex).findAll(this).map { it.value }.toList() 55 | } 56 | 57 | fun String?.findOne(@Language("RegExp") regex: String): String? = find(regex).firstOrNull() 58 | 59 | fun String.trimAllLines(): String = run { 60 | trim().lineSequence().map { it.trim() }.joinToString("\n") 61 | } 62 | -------------------------------------------------------------------------------- /honoka-kotlin-utils/src/main/java/de/honoka/sdk/util/kotlin/text/Xml.kt: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.util.kotlin.text 2 | 3 | import de.honoka.sdk.util.text.XmlUtils 4 | import org.dom4j.Element 5 | import org.intellij.lang.annotations.Language 6 | 7 | fun Element.addElementByStr(@Language("XML") str: String): Element { 8 | val element = XmlUtils.parseElement(str) 9 | add(element) 10 | return element 11 | } -------------------------------------------------------------------------------- /honoka-kotlin-utils/src/main/java/de/honoka/sdk/util/kotlin/various/RuntimeUtilsExt.kt: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.util.kotlin.various 2 | 3 | import de.honoka.sdk.util.various.RuntimeUtils 4 | 5 | object RuntimeUtilsExt { 6 | 7 | inline fun exec(block: RuntimeUtils.Commands.() -> Unit): String = run { 8 | RuntimeUtils.exec(RuntimeUtils.Commands().apply(block)) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /honoka-kotlin-utils/src/main/resources/http/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kosaka-bun/honoka-sdk/7bc41d75805cae4f98d5e141b84d37f1dd22dd32/honoka-kotlin-utils/src/main/resources/http/.gitkeep -------------------------------------------------------------------------------- /honoka-kotlin-utils/src/main/resources/http/static-headers/api.json: -------------------------------------------------------------------------------- 1 | { 2 | "Accept": "*/*", 3 | "Sec-Fetch-Dest": "empty", 4 | "Sec-Fetch-Mode": "cors", 5 | "Sec-Fetch-Site": "same-origin", 6 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", 7 | "sec-ch-ua": "\"Google Chrome\";v=\"131\", \"Chromium\";v=\"131\", \"Not_A Brand\";v=\"24\"", 8 | "sec-ch-ua-mobile": "?0", 9 | "sec-ch-ua-platform": "\"Windows\"", 10 | "Accept-Encoding": "br, deflate, gzip" 11 | } 12 | -------------------------------------------------------------------------------- /honoka-kotlin-utils/src/main/resources/http/static-headers/document.json: -------------------------------------------------------------------------------- 1 | { 2 | "Accept": "*/*", 3 | "Sec-Fetch-Dest": "document", 4 | "Sec-Fetch-Mode": "navigate", 5 | "Sec-Fetch-Site": "none", 6 | "Sec-Fetch-User": "?1", 7 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", 8 | "sec-ch-ua": "\"Google Chrome\";v=\"131\", \"Chromium\";v=\"131\", \"Not_A Brand\";v=\"24\"", 9 | "sec-ch-ua-mobile": "?0", 10 | "sec-ch-ua-platform": "\"Windows\"", 11 | "Accept-Encoding": "br, deflate, gzip" 12 | } 13 | -------------------------------------------------------------------------------- /honoka-spring-boot-starter/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import de.honoka.gradle.buildsrc.MavenPublish.setupVersionAndPublishing 2 | import de.honoka.gradle.buildsrc.implementationApi 3 | 4 | @Suppress("DSL_SCOPE_VIOLATION") 5 | plugins { 6 | alias(libs.plugins.kotlin.spring) 7 | } 8 | 9 | setupVersionAndPublishing(libs.versions.honoka.spring.boot.starter.get()) 10 | 11 | java { 12 | sourceCompatibility = JavaVersion.VERSION_17 13 | targetCompatibility = sourceCompatibility 14 | } 15 | 16 | dependencyManagement { 17 | imports { 18 | //必须按照顺序导入,后导入的依赖配置将覆盖先导入的相同依赖的配置 19 | mavenBom(libs.spring.boot.dependencies.get().toString()) 20 | mavenBom(libs.kotlin.bom.get().toString()) 21 | } 22 | } 23 | 24 | dependencies { 25 | implementationApi("de.honoka.sdk:honoka-kotlin-utils:1.1.2-dev") 26 | compileOnly("org.springframework.boot:spring-boot-starter") 27 | compileOnly("org.springframework.boot:spring-boot-starter-web") 28 | compileOnly("org.springframework.boot:spring-boot-starter-aop") 29 | compileOnly("org.springframework.boot:spring-boot-starter-security") 30 | compileOnly("com.baomidou:mybatis-plus-spring-boot3-starter:3.5.5") 31 | kapt("org.springframework.boot:spring-boot-configuration-processor") 32 | } 33 | 34 | tasks { 35 | compileKotlin { 36 | dependsOn(":honoka-kotlin-utils:publish") 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /honoka-spring-boot-starter/src/main/java/de/honoka/sdk/spring/starter/HonokaStarter.kt: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.spring.starter 2 | 3 | import org.springframework.boot.autoconfigure.AutoConfiguration 4 | import org.springframework.context.annotation.ComponentScan 5 | 6 | @ComponentScan("de.honoka.sdk.spring.starter.config") 7 | @AutoConfiguration 8 | class HonokaStarter 9 | -------------------------------------------------------------------------------- /honoka-spring-boot-starter/src/main/java/de/honoka/sdk/spring/starter/config/MainConfig.kt: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.spring.starter.config 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties 4 | import org.springframework.boot.context.properties.EnableConfigurationProperties 5 | import org.springframework.context.annotation.ComponentScan 6 | import org.springframework.context.annotation.Configuration 7 | 8 | @ComponentScan("de.honoka.sdk.spring.starter.core") 9 | @EnableConfigurationProperties(MainProperties::class) 10 | @Configuration("${MainConfig.STARTER_BEAN_NAME_PREFIX}MainConfig") 11 | class MainConfig { 12 | 13 | companion object { 14 | 15 | const val STARTER_BEAN_NAME_PREFIX = "honokaStarter" 16 | } 17 | } 18 | 19 | @ConfigurationProperties(MainProperties.PREFIX) 20 | class MainProperties { 21 | 22 | companion object { 23 | 24 | const val PREFIX = "honoka.starter" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /honoka-spring-boot-starter/src/main/java/de/honoka/sdk/spring/starter/config/MybatisPlusConfig.kt: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.spring.starter.config 2 | 3 | import com.baomidou.mybatisplus.annotation.DbType 4 | import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor 5 | import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor 6 | import de.honoka.sdk.util.kotlin.basic.log 7 | import org.springframework.beans.factory.annotation.Value 8 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty 9 | import org.springframework.boot.context.properties.ConfigurationProperties 10 | import org.springframework.boot.context.properties.EnableConfigurationProperties 11 | import org.springframework.context.annotation.Bean 12 | import org.springframework.context.annotation.ComponentScan 13 | import org.springframework.context.annotation.Configuration 14 | 15 | @ComponentScan("de.honoka.sdk.spring.starter.mybatis") 16 | @EnableConfigurationProperties(MybatisPlusProperties::class) 17 | @ConditionalOnProperty(prefix = MybatisPlusProperties.PREFIX, name = ["enabled"]) 18 | @Configuration("${MainConfig.STARTER_BEAN_NAME_PREFIX}MybatisPlusConfig") 19 | class MybatisPlusConfig(private val mybatisPlusProperties: MybatisPlusProperties) { 20 | 21 | @Value("\${spring.datasource.driver-class-name}") 22 | private var jdbcDriverClassName: String? = null 23 | 24 | @Bean 25 | fun mybatisPlusInterceptor(): MybatisPlusInterceptor = MybatisPlusInterceptor().apply { 26 | val dbType = mybatisPlusProperties.dbType ?: jdbcDriverClassName?.lowercase()?.run { 27 | DbType.values().firstOrNull { contains(it.db.lowercase()) } 28 | } ?: DbType.OTHER 29 | log.info("Used DbType: ${dbType.name}") 30 | addInnerInterceptor(PaginationInnerInterceptor(dbType)) 31 | } 32 | } 33 | 34 | @ConfigurationProperties(MybatisPlusProperties.PREFIX) 35 | data class MybatisPlusProperties( 36 | 37 | var enabled: Boolean = false, 38 | 39 | var dbType: DbType? = null 40 | ) { 41 | 42 | companion object { 43 | 44 | const val PREFIX = "${MainProperties.PREFIX}.mybatis" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /honoka-spring-boot-starter/src/main/java/de/honoka/sdk/spring/starter/config/SecurityConfig.kt: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.spring.starter.config 2 | 3 | import de.honoka.sdk.spring.starter.security.AccessDeniedHandlerImpl 4 | import de.honoka.sdk.spring.starter.security.AuthenticationEntryPointImpl 5 | import de.honoka.sdk.spring.starter.security.DefaultAuthorizationFilter 6 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty 7 | import org.springframework.boot.context.properties.ConfigurationProperties 8 | import org.springframework.boot.context.properties.EnableConfigurationProperties 9 | import org.springframework.context.annotation.Bean 10 | import org.springframework.context.annotation.ComponentScan 11 | import org.springframework.context.annotation.Configuration 12 | import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity 13 | import org.springframework.security.config.annotation.web.builders.HttpSecurity 14 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity 15 | import org.springframework.security.core.userdetails.UserDetailsService 16 | import org.springframework.security.provisioning.InMemoryUserDetailsManager 17 | import org.springframework.security.web.SecurityFilterChain 18 | import org.springframework.security.web.access.intercept.AuthorizationFilter 19 | import org.springframework.security.web.util.matcher.AntPathRequestMatcher 20 | import org.springframework.web.cors.CorsConfiguration 21 | import org.springframework.web.cors.UrlBasedCorsConfigurationSource 22 | 23 | @EnableMethodSecurity 24 | @EnableWebSecurity 25 | @ComponentScan("de.honoka.sdk.spring.starter.security") 26 | @EnableConfigurationProperties(SecurityProperties::class) 27 | @ConditionalOnProperty(prefix = SecurityProperties.PREFIX, name = ["enabled"]) 28 | @Configuration("${MainConfig.STARTER_BEAN_NAME_PREFIX}SecurityConfig") 29 | class SecurityConfig(private val securityProperties: SecurityProperties) { 30 | 31 | @Bean 32 | fun securityFilterChain(http: HttpSecurity): SecurityFilterChain = http.run { 33 | val whiteList = securityProperties.whiteList + "/error" 34 | cors { 35 | it.configurationSource(UrlBasedCorsConfigurationSource().apply { 36 | registerCorsConfiguration("/**", CorsConfiguration().apply { 37 | allowCredentials = true 38 | addAllowedHeader("*") 39 | addAllowedMethod("*") 40 | securityProperties.corsOrigins.forEach { s -> 41 | //必须以域名结尾,包含带端口和不带端口两种情况 42 | addAllowedOriginPattern("*://*$s") 43 | addAllowedOriginPattern("*://*$s:*") 44 | } 45 | }) 46 | }) 47 | } 48 | csrf { 49 | /* 50 | * CSRF攻击防护仅在由服务端从Cookie中取得token时有意义,若服务端不从请求方提供的Cookie中 51 | * 获取token,那么可以禁用CSRF防护。 52 | * 此外,若所有接口都接受JSON格式的参数,那么通过在网页上嵌入form表单的方式来发起的POST 53 | * 请求不会被后端处理,但通过fetch或ajax可以发起跨站请求,需验证这种请求是否会携带目标站点 54 | * 的Cookie。 55 | */ 56 | it.disable() 57 | } 58 | //添加能够识别自定义登录态,并将其放入SecurityContextHolder中的处理器 59 | addFilterBefore(DefaultAuthorizationFilter, AuthorizationFilter::class.java) 60 | logout { 61 | it.disable() 62 | } 63 | //设置自定义的Security异常处理器,用于处理未登录、无权访问等情况 64 | exceptionHandling { 65 | /* 66 | * 定义认证入口点,当AuthorizationFilter检测到SecurityContextHolder的context中没有 67 | * authentication信息时,则调用此处配置的authenticationEntryPoint中的方法,来执行开发者 68 | * 定义的后续行为。 69 | */ 70 | it.authenticationEntryPoint(AuthenticationEntryPointImpl) 71 | it.accessDeniedHandler(AccessDeniedHandlerImpl) 72 | } 73 | authorizeHttpRequests { 74 | val whiteListMatchers = whiteList.map { s -> AntPathRequestMatcher.antMatcher(s) }.toTypedArray() 75 | it.requestMatchers(*whiteListMatchers).permitAll() 76 | it.anyRequest().authenticated() 77 | } 78 | build() 79 | } 80 | 81 | //不使用UserDetailsService,为了防止Spring Security自动在控制台生成临时密码,使用此默认配置 82 | @Bean 83 | fun userDetailService(): UserDetailsService = InMemoryUserDetailsManager() 84 | } 85 | 86 | @ConfigurationProperties(SecurityProperties.PREFIX) 87 | data class SecurityProperties( 88 | 89 | var enabled: Boolean = false, 90 | 91 | var whiteList: List = listOf(), 92 | 93 | var corsOrigins: List = listOf(), 94 | 95 | var token: Token = Token() 96 | ) { 97 | 98 | companion object { 99 | 100 | const val PREFIX = "${MainProperties.PREFIX}.security" 101 | } 102 | 103 | data class Token( 104 | 105 | var jwtKey: String = "abcde12345", 106 | 107 | var name: String = "token", 108 | 109 | var tempName: String = "temp-token" 110 | ) 111 | } 112 | -------------------------------------------------------------------------------- /honoka-spring-boot-starter/src/main/java/de/honoka/sdk/spring/starter/core/aop/AspectUtils.java: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.spring.starter.core.aop; 2 | 3 | import org.aspectj.lang.JoinPoint; 4 | import org.aspectj.lang.reflect.CodeSignature; 5 | 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | 9 | /** 10 | * AOP类使用的工具方法 11 | */ 12 | public class AspectUtils { 13 | 14 | /** 15 | * 提取JointPoint中的,切点对应方法所获得的所有参数和参数值,到Map中。 16 | * 获取参数Map集合,可以获取到切点中的方法在被调用时,所获取到的参数与对应值。 17 | */ 18 | public static Map getNameAndValue(JoinPoint joinPoint) { 19 | Map param = new HashMap<>(); 20 | Object[] paramValues = joinPoint.getArgs(); 21 | String[] paramNames = ((CodeSignature) joinPoint.getSignature()).getParameterNames(); 22 | for(int i = 0; i < paramNames.length; i++) { 23 | param.put(paramNames[i], paramValues[i]); 24 | } 25 | return param; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /honoka-spring-boot-starter/src/main/java/de/honoka/sdk/spring/starter/core/context/ApplicationContextHolder.kt: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.spring.starter.core.context 2 | 3 | import de.honoka.sdk.spring.starter.core.context.ApplicationContextHolder.context 4 | import org.springframework.context.ApplicationContext 5 | import org.springframework.context.ApplicationContextAware 6 | import org.springframework.stereotype.Component 7 | import kotlin.reflect.KClass 8 | 9 | object ApplicationContextHolder { 10 | 11 | @Component 12 | class Injector : ApplicationContextAware { 13 | 14 | override fun setApplicationContext(applicationContext: ApplicationContext) { 15 | context = applicationContext 16 | } 17 | } 18 | 19 | lateinit var context: ApplicationContext 20 | } 21 | 22 | val KClass.springBean: T 23 | get() = context.getBean(java) 24 | -------------------------------------------------------------------------------- /honoka-spring-boot-starter/src/main/java/de/honoka/sdk/spring/starter/core/context/ConditionalComponent.kt: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.spring.starter.core.context 2 | 3 | import kotlin.reflect.KClass 4 | 5 | /** 6 | * 用于表示这个类被哪个配置类所按条件加载(仅作为标记) 7 | */ 8 | @MustBeDocumented 9 | @Retention(AnnotationRetention.SOURCE) 10 | @Target(AnnotationTarget.TYPE) 11 | annotation class ConditionalComponent(val value: KClass<*>) 12 | -------------------------------------------------------------------------------- /honoka-spring-boot-starter/src/main/java/de/honoka/sdk/spring/starter/core/context/Config.kt: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.spring.starter.core.context 2 | 3 | import org.springframework.core.env.PropertyResolver 4 | import kotlin.reflect.KClass 5 | import kotlin.reflect.full.functions 6 | 7 | /** 8 | * 解决PropertyResolver.getProperty()无法获取到yaml中的List类型的属性值的问题 9 | */ 10 | @Suppress("UNCHECKED_CAST") 11 | fun PropertyResolver.getListProperty(key: String): List? { 12 | getProperty(key, List::class.java)?.let { return it as List } 13 | val list = ArrayList() 14 | var i = 0 15 | while(true) { 16 | val aValue = getProperty("$key[$i]") ?: break 17 | list.add(aValue) 18 | i++ 19 | } 20 | return if(list.isEmpty()) null else list 21 | } 22 | 23 | @Suppress("UNCHECKED_CAST") 24 | fun > PropertyResolver.getEnumListProperty(key: String, clazz: KClass): List? { 25 | val list = getListProperty(key) ?: return null 26 | val valueOfFun = clazz.functions.first { f -> f.name == "valueOf" } 27 | fun find(name: String): T? = runCatching { valueOfFun.call(name) as T }.getOrNull() 28 | return list.map { (find(it) ?: find(it.uppercase()))!! } 29 | } -------------------------------------------------------------------------------- /honoka-spring-boot-starter/src/main/java/de/honoka/sdk/spring/starter/core/database/AbstractEmbeddedDatabaseUtils.java: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.spring.starter.core.database; 2 | 3 | import cn.hutool.core.util.StrUtil; 4 | import de.honoka.sdk.util.file.AbstractEnvironmentPathUtils; 5 | 6 | import java.io.File; 7 | import java.nio.file.Paths; 8 | 9 | public abstract class AbstractEmbeddedDatabaseUtils { 10 | 11 | public enum DbType { 12 | 13 | SQLITE, H2 14 | } 15 | 16 | private final AbstractEnvironmentPathUtils environmentPathUtils; 17 | 18 | public AbstractEmbeddedDatabaseUtils(AbstractEnvironmentPathUtils environmentPathUtils) { 19 | this.environmentPathUtils = environmentPathUtils; 20 | } 21 | 22 | /** 23 | * 获取相对于Java应用数据目录(由{@link AbstractEnvironmentPathUtils#getDataDirPathOfApp 24 | * AbstractEnvironmentPathUtils.getDataDirPathOfApp}方法获得的一个自定义目录)的嵌入式数据库的JDBC URL 25 | */ 26 | public String getJdbcUrlRelatedWithDataDir(DbType dbType, String databaseFilePath) { 27 | String dataDirPath = environmentPathUtils.getDataDirPathOfApp().replace("\\", "/"); 28 | if(!dataDirPath.endsWith("/")) dataDirPath += "/"; 29 | databaseFilePath = StrUtil.strip(databaseFilePath, "/\\", ""); 30 | String absoluteDatabaseFilePath = dataDirPath + databaseFilePath; 31 | String absoluteDatabaseFileDirPath = absoluteDatabaseFilePath.substring(0, absoluteDatabaseFilePath.lastIndexOf("/")); 32 | File absoluteDatabaseFileDir = Paths.get(absoluteDatabaseFileDirPath).toFile(); 33 | if(!absoluteDatabaseFileDir.exists()) absoluteDatabaseFileDir.mkdirs(); 34 | @SuppressWarnings("UnnecessaryLocalVariable") 35 | String jdbcUrl = switch(dbType) { 36 | case SQLITE -> "jdbc:sqlite:" + absoluteDatabaseFilePath + ".db"; 37 | case H2 -> "jdbc:h2:" + absoluteDatabaseFilePath + ";auto_server=true"; 38 | }; 39 | return jdbcUrl; 40 | } 41 | 42 | public void setJdbcUrlRelatedWithDataDirInJvmProps(DbType dbType, String databaseFilePath) { 43 | String propKey = "spring.datasource.url"; 44 | if(StrUtil.isNotBlank(System.getProperty(propKey))) return; 45 | String jdbcUrl = getJdbcUrlRelatedWithDataDir(dbType, databaseFilePath); 46 | System.setProperty(propKey, jdbcUrl); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /honoka-spring-boot-starter/src/main/java/de/honoka/sdk/spring/starter/core/gui/SpringBootConsoleWindow.java: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.spring.starter.core.gui; 2 | 3 | import de.honoka.sdk.util.basic.ThrowsRunnable; 4 | import de.honoka.sdk.util.gui.ConsoleWindow; 5 | import de.honoka.sdk.util.gui.ConsoleWindowBuilder; 6 | import lombok.AccessLevel; 7 | import lombok.Getter; 8 | import lombok.Setter; 9 | import lombok.experimental.Accessors; 10 | import org.springframework.boot.SpringApplication; 11 | import org.springframework.context.ApplicationContext; 12 | 13 | import java.util.function.Consumer; 14 | 15 | @Getter 16 | @Setter 17 | @Accessors(chain = true) 18 | public class SpringBootConsoleWindow { 19 | 20 | @Setter(AccessLevel.NONE) 21 | private ConsoleWindow consoleWindow; 22 | 23 | @Getter(AccessLevel.NONE) 24 | @Setter(AccessLevel.NONE) 25 | private ConsoleWindowBuilder consoleWindowBuilder; 26 | 27 | private Class springBootMainClass; 28 | 29 | private String[] applicationArgs = new String[0]; 30 | 31 | private ThrowsRunnable beforeRunApplication = () -> {}; 32 | 33 | private Consumer onExit = context -> {}; 34 | 35 | @Setter(AccessLevel.NONE) 36 | private ApplicationContext context; 37 | 38 | private SpringBootConsoleWindow() {} 39 | 40 | @SuppressWarnings("SameParameterValue") 41 | public static SpringBootConsoleWindow of( 42 | String windowName, double screenZoomScale, Class mainClass, String[] args 43 | ) { 44 | SpringBootConsoleWindow springBootConsoleWindow = new SpringBootConsoleWindow(); 45 | springBootConsoleWindow.consoleWindowBuilder = ConsoleWindow.Builder.of() 46 | .setWindowName(windowName) 47 | .setScreenZoomScale(screenZoomScale) 48 | .setBackgroundMode(true) 49 | .setOnExit(() -> springBootConsoleWindow.onExit.accept(springBootConsoleWindow.context)); 50 | springBootConsoleWindow.springBootMainClass = mainClass; 51 | springBootConsoleWindow.applicationArgs = args; 52 | return springBootConsoleWindow; 53 | } 54 | 55 | public static SpringBootConsoleWindow of(String windowName, double screenZoomScale, Class mainClass) { 56 | return of(windowName, screenZoomScale, mainClass, new String[0]); 57 | } 58 | 59 | public static SpringBootConsoleWindow of(double screenZoomScale, Class mainClass) { 60 | return of(mainClass.getSimpleName(), screenZoomScale, mainClass); 61 | } 62 | 63 | public static SpringBootConsoleWindow of(Class mainClass) { 64 | return of(1.0, mainClass); 65 | } 66 | 67 | public SpringBootConsoleWindow configureWindowBuilder(Consumer configurer) { 68 | configurer.accept(consoleWindowBuilder); 69 | return this; 70 | } 71 | 72 | /** 73 | * 创建窗口,并启动SpringBoot应用 74 | */ 75 | public void createAndRun() { 76 | consoleWindow = consoleWindowBuilder.build(); 77 | beforeRunApplication.run(); 78 | SpringApplication app = new SpringApplication(springBootMainClass); 79 | context = app.run(applicationArgs); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /honoka-spring-boot-starter/src/main/java/de/honoka/sdk/spring/starter/core/web/GlobalExceptionHandler.kt: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.spring.starter.core.web 2 | 3 | import cn.hutool.core.exceptions.ExceptionUtil 4 | import de.honoka.sdk.util.kotlin.basic.isAnyType 5 | import de.honoka.sdk.util.kotlin.basic.log 6 | import de.honoka.sdk.util.web.ApiResponse 7 | import jakarta.servlet.http.HttpServletRequest 8 | import jakarta.servlet.http.HttpServletResponse 9 | import org.springframework.http.HttpStatus 10 | import org.springframework.web.bind.MethodArgumentNotValidException 11 | import org.springframework.web.bind.annotation.ExceptionHandler 12 | import org.springframework.web.bind.annotation.RestControllerAdvice 13 | import org.springframework.web.servlet.resource.NoResourceFoundException 14 | import kotlin.reflect.KClass 15 | 16 | @RestControllerAdvice 17 | class GlobalExceptionHandler { 18 | 19 | private val disablePrintLogExceptionTypes = listOf>( 20 | MethodArgumentNotValidException::class, 21 | NoResourceFoundException::class 22 | ) 23 | 24 | @ExceptionHandler 25 | fun handle(t: Throwable, request: HttpServletRequest, response: HttpServletResponse): ApiResponse<*>? { 26 | if(!t.isAnyType(disablePrintLogExceptionTypes)) { 27 | log.error("", t) 28 | } 29 | response.status = HttpStatus.INTERNAL_SERVER_ERROR.value() 30 | if(!request.canAcceptJson()) return null 31 | val msg = if(t.message?.isNotBlank() == true) { 32 | t.message 33 | } else { 34 | ExceptionUtil.getMessage(t) 35 | } 36 | return ApiResponse.fail(msg) 37 | } 38 | 39 | @ExceptionHandler 40 | fun handle( 41 | e: MethodArgumentNotValidException, 42 | request: HttpServletRequest, 43 | response: HttpServletResponse 44 | ): ApiResponse<*>? { 45 | val message = e.allErrors.map { it.defaultMessage }.joinToString() 46 | return handle(IllegalArgumentException(message), request, response) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /honoka-spring-boot-starter/src/main/java/de/honoka/sdk/spring/starter/core/web/HttpServlet.kt: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.spring.starter.core.web 2 | 3 | import jakarta.servlet.http.Cookie 4 | import jakarta.servlet.http.HttpServletRequest 5 | import org.springframework.http.HttpHeaders 6 | import org.springframework.http.MediaType 7 | 8 | /** 9 | * 从请求中获取请求方的真实IP,忽略反向代理 10 | */ 11 | val HttpServletRequest.clientRealIp: String? 12 | get() { 13 | //反向代理后,转发请求的HTTP头信息中,增加了X-Real-IP等信息 14 | val headerNames = listOf("X-Real-IP", "X-Forwarded-For", "Proxy-Client-IP", "WL-Proxy-Client-IP") 15 | var ip: String? = null 16 | for(it in headerNames) { 17 | ip = getHeader(it) 18 | if(ip?.isNotBlank() == true && !ip.equals("unknown", true)) break 19 | ip = null 20 | } 21 | ip = ip ?: remoteAddr ?: return null 22 | if(ip.contains(",")) { 23 | ip = ip.split(",")[0] 24 | } 25 | if(ip in listOf("::1", "0:0:0:0:0:0:0:1")) { 26 | return "127.0.0.1" 27 | } 28 | return ip 29 | } 30 | 31 | /** 32 | * 长度为2的`List`,第一个元素为Token类型(如Bearer),第二个元素为Token值 33 | */ 34 | val HttpServletRequest.authorization: List 35 | get() = run { 36 | getHeader(HttpHeaders.AUTHORIZATION)?.split(" ")?.run { 37 | if(size < 2) listOf(null, first()) else this 38 | } ?: listOf(null, null) 39 | } 40 | 41 | operator fun Array?.get(name: String): String? = this?.firstOrNull { it.name == name }?.value 42 | 43 | fun HttpServletRequest.canAcceptJson(): Boolean { 44 | val accept = getHeader(HttpHeaders.ACCEPT) 45 | if(accept.contains(MediaType.APPLICATION_JSON_VALUE)) { 46 | return true 47 | } 48 | if(accept.contains(MediaType.ALL_VALUE)) { 49 | return !accept.contains(MediaType.TEXT_HTML_VALUE) 50 | } 51 | return false 52 | } 53 | -------------------------------------------------------------------------------- /honoka-spring-boot-starter/src/main/java/de/honoka/sdk/spring/starter/core/web/WebUtils.kt: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.spring.starter.core.web 2 | 3 | /** 4 | * Web相关工具 5 | */ 6 | object WebUtils { 7 | 8 | fun cookieStringToMap(cookieString: String): Map { 9 | val map = HashMap() 10 | cookieString.split("; ").forEach { 11 | val separateIndex = it.indexOf("=") 12 | if(separateIndex < 0) return@forEach 13 | val key = it.substring(0, separateIndex) 14 | val value = if(separateIndex < it.length - 1) { 15 | it.substring(separateIndex + 1) 16 | } else { 17 | "" 18 | } 19 | map[key] = value 20 | } 21 | return map 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /honoka-spring-boot-starter/src/main/java/de/honoka/sdk/spring/starter/mybatis/QueryWrapper.kt: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.spring.starter.mybatis 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper 4 | import com.baomidou.mybatisplus.extension.kotlin.KtQueryChainWrapper 5 | import com.baomidou.mybatisplus.extension.kotlin.KtUpdateChainWrapper 6 | import com.baomidou.mybatisplus.extension.toolkit.ChainWrappers 7 | 8 | inline fun BaseMapper.queryChainWrapper(): KtQueryChainWrapper = run { 9 | ChainWrappers.ktQueryChain(this, T::class.java) 10 | } 11 | 12 | inline fun BaseMapper.updateChainWrapper(): KtUpdateChainWrapper = run { 13 | ChainWrappers.ktUpdateChain(this, T::class.java) 14 | } 15 | -------------------------------------------------------------------------------- /honoka-spring-boot-starter/src/main/java/de/honoka/sdk/spring/starter/security/DefaultAuthorizationFilter.kt: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.spring.starter.security 2 | 3 | import cn.hutool.json.JSONObject 4 | import cn.hutool.json.JSONUtil 5 | import de.honoka.sdk.spring.starter.config.SecurityProperties 6 | import de.honoka.sdk.spring.starter.core.context.springBean 7 | import de.honoka.sdk.spring.starter.core.web.authorization 8 | import de.honoka.sdk.spring.starter.core.web.get 9 | import de.honoka.sdk.spring.starter.security.token.JwtUtils 10 | import de.honoka.sdk.spring.starter.security.token.TempTokenUtils 11 | import jakarta.servlet.FilterChain 12 | import jakarta.servlet.http.HttpServletRequest 13 | import jakarta.servlet.http.HttpServletResponse 14 | import org.springframework.security.authentication.AbstractAuthenticationToken 15 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken 16 | import org.springframework.security.core.context.SecurityContextHolder 17 | import org.springframework.web.filter.OncePerRequestFilter 18 | 19 | /** 20 | * 用于手动为SecurityContextHolder的context添加authentication信息 21 | */ 22 | @Suppress("MemberVisibilityCanBePrivate") 23 | object DefaultAuthorizationFilter : OncePerRequestFilter() { 24 | 25 | private val securityProperties = SecurityProperties::class.springBean 26 | 27 | override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain) { 28 | val token = request.run { authorization[1] ?: cookies[securityProperties.token.name] } 29 | val tempToken = request.cookies[securityProperties.token.tempName] 30 | when { 31 | !token.isNullOrBlank() -> tokenAuthentication(token) 32 | !tempToken.isNullOrBlank() -> tempTokenAuthentication(tempToken) 33 | } 34 | filterChain.doFilter(request, response) 35 | } 36 | 37 | private fun tokenAuthentication(token: String) { 38 | val jwt = try { 39 | JwtUtils.parseAvaliableJwt(token) 40 | } catch(t: Throwable) { 41 | return 42 | } 43 | val user = JSONUtil.toBean(jwt.payloads["user"] as? JSONObject, DefaultUser::class.java) 44 | /* 45 | * 这里必须使用三个参数的UsernamePasswordAuthenticationToken构造方法,因为两个参数的构造方法会 46 | * 将对象中的authenticated字段设为false,而三个参数的构造方法会设为true。 47 | */ 48 | val authentication = UsernamePasswordAuthenticationToken( 49 | user.username, jwt, user.toUserDetails().authorityObjects 50 | ) 51 | SecurityContextHolder.getContext().authentication = authentication.apply { 52 | details = user 53 | } 54 | } 55 | 56 | private fun tempTokenAuthentication(token: String) { 57 | runCatching { 58 | TempTokenUtils.checkToken(token) 59 | SecurityContextHolder.getContext().authentication = TempAuthenticationToken(token) 60 | } 61 | } 62 | } 63 | 64 | /** 65 | * 通过临时token获取的临时登录态。 66 | * 67 | * 注意:若authenticated被设置为true,则此登录态能够访问到在SecurityConfig中被设置为authenticated 68 | * 的URL路径,需额外考虑如何避免持有此登录态的用户访问需要普通登录态的URL路径。 69 | */ 70 | @Suppress("unused") 71 | class TempAuthenticationToken( 72 | val token: String, authenticated: Boolean = false 73 | ) : AbstractAuthenticationToken(null) { 74 | 75 | init { 76 | isAuthenticated = authenticated 77 | } 78 | 79 | override fun getCredentials(): Any? = null 80 | 81 | override fun getPrincipal(): Any? = null 82 | } -------------------------------------------------------------------------------- /honoka-spring-boot-starter/src/main/java/de/honoka/sdk/spring/starter/security/DefaultUser.kt: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.spring.starter.security 2 | 3 | import org.springframework.security.core.GrantedAuthority 4 | import org.springframework.security.core.authority.SimpleGrantedAuthority 5 | import org.springframework.security.core.userdetails.UserDetails 6 | import java.util.* 7 | 8 | open class DefaultUser { 9 | 10 | var id: Long? = null 11 | 12 | var username: String? = null 13 | 14 | var password: String? = null 15 | 16 | var authorities: String? = null 17 | 18 | var enabled: Boolean? = null 19 | 20 | var locked: Boolean? = null 21 | 22 | var expireTime: Date? = null 23 | 24 | var credentialsExpireTime: Date? = null 25 | 26 | fun toUserDetails(): DefaultUserDetails = DefaultUserDetails(this) 27 | } 28 | 29 | @Suppress("MemberVisibilityCanBePrivate") 30 | open class DefaultUserDetails(private val user: DefaultUser) : UserDetails { 31 | 32 | val authorityList: List? 33 | get() = user.authorities?.split(",") 34 | 35 | val authorityObjects: List? 36 | get() = authorityList?.map { SimpleGrantedAuthority(it) } 37 | 38 | override fun getUsername(): String? = user.username 39 | 40 | override fun getPassword(): String? = user.password 41 | 42 | override fun getAuthorities(): MutableCollection? = authorityObjects?.toMutableList() 43 | 44 | override fun isEnabled(): Boolean = user.enabled == true 45 | 46 | override fun isAccountNonLocked(): Boolean = user.locked == false 47 | 48 | override fun isAccountNonExpired(): Boolean = run { 49 | System.currentTimeMillis() < (user.expireTime?.time ?: Long.MAX_VALUE) 50 | } 51 | 52 | override fun isCredentialsNonExpired(): Boolean = run { 53 | System.currentTimeMillis() < (user.credentialsExpireTime?.time ?: Long.MAX_VALUE) 54 | } 55 | } -------------------------------------------------------------------------------- /honoka-spring-boot-starter/src/main/java/de/honoka/sdk/spring/starter/security/SecurityExceptionHandler.kt: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.spring.starter.security 2 | 3 | import cn.hutool.core.exceptions.ExceptionUtil 4 | import cn.hutool.json.JSONObject 5 | import de.honoka.sdk.spring.starter.core.web.canAcceptJson 6 | import de.honoka.sdk.util.web.ApiResponse 7 | import jakarta.servlet.http.HttpServletRequest 8 | import jakarta.servlet.http.HttpServletResponse 9 | import org.springframework.http.HttpHeaders 10 | import org.springframework.http.HttpStatus 11 | import org.springframework.http.MediaType 12 | import org.springframework.security.access.AccessDeniedException 13 | import org.springframework.security.core.AuthenticationException 14 | import org.springframework.security.web.AuthenticationEntryPoint 15 | import org.springframework.security.web.access.AccessDeniedHandler 16 | import org.springframework.web.bind.annotation.ExceptionHandler 17 | import org.springframework.web.bind.annotation.RestControllerAdvice 18 | 19 | @RestControllerAdvice 20 | class SecurityExceptionHandler { 21 | 22 | @ExceptionHandler 23 | fun handle(e: AccessDeniedException, request: HttpServletRequest, response: HttpServletResponse) { 24 | AccessDeniedHandlerImpl.handle(request, response, e) 25 | } 26 | } 27 | 28 | /** 29 | * 当`ExceptionTranslationFilter`之后存在Filter抛出`AccessDeniedException`时,`ExceptionTranslationFilter` 30 | * 会检查`SecurityContextHolder`的`context`中是否存在`authentication`信息,若不存在,则视为请求方未登录,调用 31 | * 本类中的方法对请求和响应进行处理。 32 | * 此处为返回一段JSON提示信息。 33 | */ 34 | object AuthenticationEntryPointImpl : AuthenticationEntryPoint { 35 | 36 | override fun commence( 37 | request: HttpServletRequest, 38 | response: HttpServletResponse, 39 | authException: AuthenticationException? 40 | ) { 41 | respondError(request, response, HttpStatus.UNAUTHORIZED, "未登录或Token已失效", authException) 42 | } 43 | } 44 | 45 | /** 46 | * 当ExceptionTranslationFilter之后存在Filter抛出AccessDeniedException时,ExceptionTranslationFilter 47 | * 会检查SecurityContextHolder的context中是否存在authentication信息,若存在,则视为请求方已登录但无权访问 48 | * 指定的路径,调用本类中的方法对请求和响应进行处理。 49 | * 此处为返回一段JSON提示信息。 50 | */ 51 | object AccessDeniedHandlerImpl : AccessDeniedHandler { 52 | 53 | override fun handle( 54 | request: HttpServletRequest, 55 | response: HttpServletResponse, 56 | accessDeniedException: AccessDeniedException? 57 | ) { 58 | respondError(request, response, HttpStatus.FORBIDDEN, "访问被拒绝", accessDeniedException) 59 | } 60 | } 61 | 62 | private fun respondError( 63 | request: HttpServletRequest, response: HttpServletResponse, 64 | status: HttpStatus, msg: String, exception: Throwable? 65 | ) { 66 | response.status = status.value() 67 | if(!request.canAcceptJson()) return 68 | response.addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) 69 | response.outputStream.writer(Charsets.UTF_8).use { 70 | val apiResponse = ApiResponse.of().also { ar -> 71 | ar.code = status.value() 72 | ar.status = false 73 | ar.msg = msg 74 | ar.data = JSONObject().also { jo -> 75 | jo["exception"] = ExceptionUtil.getMessage(exception) 76 | } 77 | } 78 | it.write(apiResponse.toJsonString()) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /honoka-spring-boot-starter/src/main/java/de/honoka/sdk/spring/starter/security/token/JwtUtils.kt: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.spring.starter.security.token 2 | 3 | import cn.hutool.cache.CacheUtil 4 | import cn.hutool.core.bean.BeanUtil 5 | import cn.hutool.core.date.DateField 6 | import cn.hutool.core.date.DateTime 7 | import cn.hutool.jwt.JWT 8 | import de.honoka.sdk.spring.starter.config.SecurityProperties 9 | import de.honoka.sdk.spring.starter.core.context.springBean 10 | import de.honoka.sdk.spring.starter.security.DefaultUser 11 | import org.springframework.security.core.context.SecurityContextHolder 12 | import org.springframework.security.web.authentication.rememberme.InvalidCookieException 13 | import java.util.concurrent.TimeUnit 14 | 15 | object JwtUtils { 16 | 17 | @Suppress("MemberVisibilityCanBePrivate") 18 | var key: String = SecurityProperties::class.springBean.token.jwtKey 19 | 20 | private val tokenCache = CacheUtil.newTimedCache(0).apply { 21 | /** 22 | * 设置间隔多长时间主动清理过期缓存,防止过期缓存长时间未读取而滞留在内存中。 23 | * 24 | * 需要注意的是,并不是过期缓存没有被主动清理就意味着缓存没有过期,每次从缓存中取值时,即使取到了 25 | * 值也会再次检查取到的值是否已过期,如果已过期将会立刻清理此值。 26 | * @see cn.hutool.cache.impl.StampedCache.get 27 | */ 28 | schedulePrune(TimeUnit.HOURS.toMillis(1)) 29 | } 30 | 31 | private val JWT.cacheKey 32 | get() = "${payloads.getByPath("user.id")}-${payloads["iat"]}" 33 | 34 | fun newJwt(user: DefaultUser, periodDays: Int = 7): String = JWT.create().run { 35 | setKey(key.toByteArray()) 36 | val payload = mapOf("user" to BeanUtil.beanToMap(user)) 37 | addPayloads(payload) 38 | val now = DateTime.now() 39 | val expireAt = now.offsetNew(DateField.DAY_OF_YEAR, periodDays) 40 | setIssuedAt(now) 41 | setNotBefore(now) 42 | setExpiresAt(expireAt) 43 | val timeout = expireAt.time - now.time 44 | tokenCache.put("${user.id}-${now.time / 1000L}", null, timeout) 45 | sign() 46 | } 47 | 48 | fun parseAvaliableJwt(token: String): JWT = JWT(token).apply { 49 | setKey(key.toByteArray()) 50 | if(!validate(0) && tokenCache.containsKey(cacheKey)) { 51 | throw InvalidCookieException("JWT无效或已过期") 52 | } 53 | } 54 | 55 | fun cancelJwt() { 56 | val jwt = SecurityContextHolder.getContext().authentication.credentials as JWT 57 | tokenCache.remove(jwt.cacheKey) 58 | } 59 | } -------------------------------------------------------------------------------- /honoka-spring-boot-starter/src/main/java/de/honoka/sdk/spring/starter/security/token/TempTokenUtils.kt: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.spring.starter.security.token 2 | 3 | import cn.hutool.cache.CacheUtil 4 | import cn.hutool.core.lang.Assert 5 | import java.util.* 6 | import java.util.concurrent.TimeUnit 7 | 8 | /** 9 | * 临时token工具,通常用于对外提供分享链接时,供使用分享链接的匿名用户使用 10 | */ 11 | @Suppress("unused") 12 | object TempTokenUtils { 13 | 14 | private val tokenCache = CacheUtil.newTimedCache(0).apply { 15 | schedulePrune(TimeUnit.HOURS.toMillis(1)) 16 | } 17 | 18 | fun newToken(periodHours: Long = 1): String { 19 | val token = UUID.randomUUID().toString() 20 | val timeout = TimeUnit.HOURS.toMillis(periodHours) 21 | tokenCache.put(token, null, timeout) 22 | return token 23 | } 24 | 25 | fun checkToken(token: String) { 26 | Assert.isTrue(tokenCache.containsKey(token), "token不存在或已过期") 27 | } 28 | 29 | fun cancelToken(token: String) { 30 | tokenCache.remove(token) 31 | } 32 | } -------------------------------------------------------------------------------- /honoka-spring-boot-starter/src/main/resources/META-INF/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kosaka-bun/honoka-sdk/7bc41d75805cae4f98d5e141b84d37f1dd22dd32/honoka-spring-boot-starter/src/main/resources/META-INF/.gitkeep -------------------------------------------------------------------------------- /honoka-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports: -------------------------------------------------------------------------------- 1 | de.honoka.sdk.spring.starter.HonokaStarter 2 | -------------------------------------------------------------------------------- /honoka-utils/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import de.honoka.gradle.buildsrc.MavenPublish.setupVersionAndPublishing 2 | import de.honoka.gradle.buildsrc.implementationApi 3 | 4 | setupVersionAndPublishing(libs.versions.honoka.utils.get()) 5 | 6 | dependencies { 7 | implementationApi("cn.hutool:hutool-all:5.8.25") 8 | implementationApi("org.dom4j:dom4j:2.1.4") 9 | implementationApi("org.jsoup:jsoup:1.18.1") 10 | implementation("gui.ava:html2image:2.0.1") { 11 | exclude("xml-apis", "xml-apis") 12 | } 13 | implementationApi(libs.slf4j.api) 14 | implementation(libs.logback) 15 | compileOnly("org.jetbrains:annotations:24.0.0") 16 | runtimeOnly("org.bouncycastle:bcprov-jdk18on:1.78.1") 17 | } 18 | -------------------------------------------------------------------------------- /honoka-utils/src/main/java/de/honoka/sdk/util/basic/ActionUtils.java: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.util.basic; 2 | 3 | public class ActionUtils { 4 | 5 | /** 6 | * 忽略异常执行一段代码 7 | */ 8 | public static void doIgnoreException(boolean printStackTrace, ThrowsRunnable action) { 9 | try { 10 | action.throwsRun(); 11 | } catch(Throwable t) { 12 | if(printStackTrace) t.printStackTrace(); 13 | } 14 | } 15 | 16 | public static void doIgnoreException(ThrowsRunnable action) { 17 | doIgnoreException(false, action); 18 | } 19 | 20 | /** 21 | * 控制台可视化执行一段代码 22 | */ 23 | public static void doAction(String name, ThrowsRunnable action) { 24 | System.out.println("开始执行" + name + "……"); 25 | try { 26 | action.throwsRun(); 27 | System.out.println(name + "执行完成"); 28 | } catch(Throwable t) { 29 | System.err.println(name + "未成功执行,错误信息如下:"); 30 | t.printStackTrace(); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /honoka-utils/src/main/java/de/honoka/sdk/util/basic/CodeUtils.java: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.util.basic; 2 | 3 | import lombok.SneakyThrows; 4 | 5 | import java.util.List; 6 | import java.util.Map; 7 | import java.util.stream.Collectors; 8 | 9 | /** 10 | * 用于简化代码的工具类 11 | */ 12 | public class CodeUtils { 13 | 14 | /** 15 | * 不受检的线程休眠方法 16 | */ 17 | @SneakyThrows 18 | public static void threadSleep(long millis) { 19 | Thread.sleep(millis); 20 | } 21 | 22 | /** 23 | * 输出所有系统配置 24 | */ 25 | public static void printSystemProperties() { 26 | List> props = System.getProperties() 27 | .entrySet() 28 | .stream().sorted( 29 | (o1, o2) -> String.CASE_INSENSITIVE_ORDER 30 | .compare(o1.toString(), o2.toString()) 31 | ).collect(Collectors.toList()); 32 | for(Map.Entry prop : props) { 33 | System.out.println(prop.getKey().toString() + "=" + prop.getValue()); 34 | System.out.println(); 35 | } 36 | } 37 | 38 | @SneakyThrows 39 | public static void sneakyThrows(Throwable t) { 40 | throw t; 41 | } 42 | 43 | public static Class getCallerClass() { 44 | StackTraceElement[] stackTrace = new Throwable().getStackTrace(); 45 | if(stackTrace.length < 3) return null; 46 | try { 47 | for(int i = 2; i < stackTrace.length; i++) { 48 | StackTraceElement element = stackTrace[i]; 49 | if(element.getMethodName().endsWith("$default")) continue; 50 | return Class.forName(element.getClassName()); 51 | } 52 | return null; 53 | } catch(Throwable t) { 54 | return null; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /honoka-utils/src/main/java/de/honoka/sdk/util/basic/ColorfulText.java: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.util.basic; 2 | 3 | @SuppressWarnings("unused") 4 | public class ColorfulText { 5 | 6 | private static final String template = "\u001B[%sm%s\u001B[0m"; 7 | 8 | private final StringBuilder text = new StringBuilder(); 9 | 10 | private ColorfulText() { 11 | } 12 | 13 | @Override 14 | public String toString() { 15 | return text.toString(); 16 | } 17 | 18 | public static ColorfulText of() { 19 | return new ColorfulText(); 20 | } 21 | 22 | public void print() { 23 | System.out.print(text); 24 | } 25 | 26 | public void println() { 27 | System.out.println(text); 28 | } 29 | 30 | private ColorfulText add(int colorCode, Object text) { 31 | this.text.append(String.format(template, colorCode, text.toString())); 32 | return this; 33 | } 34 | 35 | public ColorfulText manual(int code, Object o) { 36 | return add(code, o); 37 | } 38 | 39 | public ColorfulText white(Object o) { 40 | return add(30, o); 41 | } 42 | 43 | public ColorfulText red(Object o) { 44 | return add(31, o); 45 | } 46 | 47 | public ColorfulText green(Object o) { 48 | return add(32, o); 49 | } 50 | 51 | public ColorfulText darkYellow(Object o) { 52 | return add(33, o); 53 | } 54 | 55 | public ColorfulText blue(Object o) { 56 | return add(34, o); 57 | } 58 | 59 | public ColorfulText purple(Object o) { 60 | return add(35, o); 61 | } 62 | 63 | public ColorfulText aqua(Object o) { 64 | return add(36, o); 65 | } 66 | 67 | public ColorfulText underline(Object o) { 68 | return add(21, o); 69 | } 70 | 71 | //----------------------------------------------- 72 | 73 | public ColorfulText pink(Object o) { 74 | return add(91, o); 75 | } 76 | 77 | //32号和92号颜色相同 78 | @Deprecated 79 | public ColorfulText lightGreen(Object o) { 80 | return add(92, o); 81 | } 82 | 83 | public ColorfulText yellow(Object o) { 84 | return add(93, o); 85 | } 86 | 87 | public ColorfulText lightBlue(Object o) { 88 | return add(94, o); 89 | } 90 | 91 | public ColorfulText lightPurple(Object o) { 92 | return add(95, o); 93 | } 94 | 95 | public ColorfulText lightAqua(Object o) { 96 | return add(96, o); 97 | } 98 | 99 | public ColorfulText black(Object o) { 100 | return add(97, o); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /honoka-utils/src/main/java/de/honoka/sdk/util/basic/DateBuilder.java: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.util.basic; 2 | 3 | import java.util.Date; 4 | 5 | /** 6 | * 用于根据一个时间对象构建出需要的时间 7 | */ 8 | public class DateBuilder { 9 | 10 | /** 11 | * 利用传入的时间对象新创建的时间对象,对它进行调整不会影响原有时间对象 12 | */ 13 | private final Date date; 14 | 15 | private DateBuilder(Date date) { 16 | //如果直接使用传入的date对象,那么在对它进行修改时就会影响此传入对象原本的值 17 | this.date = new Date(date.getTime()); 18 | } 19 | 20 | private DateBuilder(long timeMillis) { 21 | this.date = new Date(timeMillis); 22 | } 23 | 24 | public static DateBuilder of(Date date) { 25 | return new DateBuilder(date); 26 | } 27 | 28 | public static DateBuilder of(long timeMillis) { 29 | return new DateBuilder(timeMillis); 30 | } 31 | 32 | /** 33 | * 在现有时间的基础上加上n秒 34 | */ 35 | public DateBuilder second(double n) { 36 | date.setTime((long) (date.getTime() + n * 1000)); 37 | return this; 38 | } 39 | 40 | public DateBuilder minute(double n) { 41 | return second(n * 60); 42 | } 43 | 44 | public DateBuilder hour(double n) { 45 | return minute(n * 60); 46 | } 47 | 48 | public DateBuilder day(double n) { 49 | return hour(n * 24); 50 | } 51 | 52 | public Date get() { 53 | return date; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /honoka-utils/src/main/java/de/honoka/sdk/util/basic/HonokaComparator.java: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.util.basic; 2 | 3 | import java.util.Comparator; 4 | 5 | public interface HonokaComparator extends Comparator { 6 | 7 | /** 8 | * 和一个传入的对象相匹配,返回较大的那个对象,一样大返回null 9 | */ 10 | T getBigger(T o1, T o2); 11 | 12 | /** 13 | * o1比o2大,返回正数,o1比o2小,返回负数,一样大返回0 14 | */ 15 | @Override 16 | default int compare(T o1, T o2) { 17 | T bigger = getBigger(o1, o2); 18 | if(bigger == o1) { 19 | return 1; 20 | } else if(bigger == o2) { 21 | return -1; 22 | } 23 | return 0; 24 | } 25 | 26 | default HonokaComparator desc() { 27 | HonokaComparator superComparator = this; 28 | return (o1, o2) -> { 29 | T bigger = superComparator.getBigger(o1, o2); 30 | return bigger == null ? null : bigger == o1 ? o2 : o1; 31 | }; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /honoka-utils/src/main/java/de/honoka/sdk/util/basic/ThrowsConsumer.java: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.util.basic; 2 | 3 | import lombok.SneakyThrows; 4 | 5 | import java.util.function.Consumer; 6 | 7 | public interface ThrowsConsumer extends Consumer { 8 | 9 | void throwsAccept(T t) throws Throwable; 10 | 11 | @SneakyThrows 12 | @Override 13 | default void accept(T t) { 14 | throwsAccept(t); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /honoka-utils/src/main/java/de/honoka/sdk/util/basic/ThrowsRunnable.java: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.util.basic; 2 | 3 | import lombok.SneakyThrows; 4 | 5 | public interface ThrowsRunnable extends Runnable { 6 | 7 | void throwsRun() throws Throwable; 8 | 9 | @SneakyThrows 10 | @Override 11 | default void run() { 12 | throwsRun(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /honoka-utils/src/main/java/de/honoka/sdk/util/basic/javadoc/NotThreadSafe.java: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.util.basic.javadoc; 2 | 3 | import java.lang.annotation.*; 4 | 5 | /** 6 | * 标识一个元素不是线程安全的。 7 | *

8 | * 如果注解在类上,表示这个类中的所有方法都不是线程安全的。类中的任何一个方法在被一个线程调用时, 9 | * 另一个线程不可以同时调用同一个方法或任何一个其他方法,即使有方法被单独用{@link ThreadSafe}注解 10 | * 所标识,也仅表示该方法本身是线程安全的,不表示该方法和非线程安全的方法可以被同时调用。 11 | *

12 | * 如果注解在方法上,表示这个方法不是线程安全的。单个方法不可以被多个线程同时调用。 13 | */ 14 | @Documented 15 | @Target({ ElementType.TYPE, ElementType.METHOD }) 16 | @Retention(RetentionPolicy.SOURCE) 17 | public @interface NotThreadSafe {} 18 | -------------------------------------------------------------------------------- /honoka-utils/src/main/java/de/honoka/sdk/util/basic/javadoc/ThreadSafe.java: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.util.basic.javadoc; 2 | 3 | import java.lang.annotation.*; 4 | 5 | /** 6 | * 标识一个元素是线程安全的。 7 | *

8 | * 如果注解在类上,表示这个类中的所有方法都是线程安全的。类中的任何一个方法在被一个线程调用时, 9 | * 另一个线程可以同时调用同一个方法或任何一个其他方法,除非某个方法被单独用{@link NotThreadSafe} 10 | * 注解所标识,如某些私有方法或包级私有方法。 11 | *

12 | * 如果注解在方法上,表示这个方法是线程安全的。单个方法可以被多个线程同时调用。 13 | */ 14 | @Documented 15 | @Target({ ElementType.TYPE, ElementType.METHOD }) 16 | @Retention(RetentionPolicy.SOURCE) 17 | public @interface ThreadSafe {} 18 | -------------------------------------------------------------------------------- /honoka-utils/src/main/java/de/honoka/sdk/util/concurrent/LockUtils.java: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.util.concurrent; 2 | 3 | import de.honoka.sdk.util.basic.CodeUtils; 4 | 5 | import java.util.Iterator; 6 | import java.util.concurrent.Callable; 7 | 8 | public class LockUtils { 9 | 10 | public static T synchronizedItems(Iterable iterable, Callable callable) { 11 | return synchronizedItems(iterable.iterator(), callable); 12 | } 13 | 14 | private static T synchronizedItems(Iterator iterator, Callable callable) { 15 | Object obj = null; 16 | while(obj == null && iterator.hasNext()) { 17 | obj = iterator.next(); 18 | } 19 | if(obj != null) { 20 | //noinspection SynchronizationOnLocalVariableOrMethodParameter 21 | synchronized(obj) { 22 | return synchronizedItems(iterator, callable); 23 | } 24 | } else { 25 | try { 26 | return callable.call(); 27 | } catch(Throwable t) { 28 | CodeUtils.sneakyThrows(t); 29 | return null; 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /honoka-utils/src/main/java/de/honoka/sdk/util/concurrent/NewThreadFirstQueue.java: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.util.concurrent; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import org.jetbrains.annotations.NotNull; 7 | 8 | import java.util.concurrent.LinkedBlockingQueue; 9 | import java.util.concurrent.ThreadPoolExecutor; 10 | 11 | /** 12 | * 用于支持{@link ThreadPoolExecutor}在线程数未达到最大线程数时创建新线程的任务队列。 13 | *

14 | * 15 | * 本类来源于Dubbo( 17 | * https://github.com/apache/dubbo/blob/3.3/dubbo-common/src/main/java/org/apache/dubbo/common 18 | * /threadpool/support/eager/TaskQueue.java 19 | * ),并进行了一些修改。 20 | * 21 | */ 22 | @Setter(AccessLevel.PACKAGE) 23 | @Getter 24 | public class NewThreadFirstQueue extends LinkedBlockingQueue { 25 | 26 | private ThreadPoolExecutor executor; 27 | 28 | public NewThreadFirstQueue(int capacity) { 29 | super(capacity); 30 | } 31 | 32 | @Override 33 | public boolean offer(@NotNull R runnable) { 34 | synchronized(this) { 35 | //have free worker. put task into queue to let the worker deal with task. 36 | if(executor.getActiveCount() + size() < executor.getPoolSize()) { 37 | return super.offer(runnable); 38 | } 39 | //return false to let executor create new worker. 40 | if(executor.getPoolSize() < executor.getMaximumPoolSize()) { 41 | return false; 42 | } 43 | } 44 | //poolSize >= max 45 | return super.offer(runnable); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /honoka-utils/src/main/java/de/honoka/sdk/util/concurrent/ThreadPoolUtils.java: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.util.concurrent; 2 | 3 | import java.util.concurrent.*; 4 | 5 | public class ThreadPoolUtils { 6 | 7 | private static final RejectedExecutionHandler defaultRejectedExecutionHandler = 8 | new ThreadPoolExecutor.AbortPolicy(); 9 | 10 | public static ScheduledThreadPoolExecutor newScheduledPool( 11 | int coreSize, RejectedExecutionHandler rejectedExecutionHandler 12 | ) { 13 | if(rejectedExecutionHandler == null) { 14 | rejectedExecutionHandler = defaultRejectedExecutionHandler; 15 | } 16 | ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(coreSize, rejectedExecutionHandler); 17 | /* 18 | * 任务取消时将定时任务的待执行单元从队列中删除,默认是false。在默认情况下,如果直接取消任务, 19 | * 并不会从队列中删除此任务的待执行单元。 20 | */ 21 | executor.setRemoveOnCancelPolicy(true); 22 | //shutdown被调用后是否还执行队列中的延迟任务 23 | executor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false); 24 | //shutdown被调用后是否继续执行正在执行的任务 25 | executor.setContinueExistingPeriodicTasksAfterShutdownPolicy(false); 26 | return executor; 27 | } 28 | 29 | /** 30 | * 使用{@link NewThreadFirstQueue}创建线程池 31 | */ 32 | public static ThreadPoolExecutor newEagerThreadPool( 33 | int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit 34 | ) { 35 | return newEagerThreadPool( 36 | corePoolSize, maximumPoolSize, keepAliveTime, unit, Integer.MAX_VALUE 37 | ); 38 | } 39 | 40 | public static ThreadPoolExecutor newEagerThreadPool( 41 | int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, 42 | int queueCapacity 43 | ) { 44 | return newEagerThreadPool( 45 | corePoolSize, maximumPoolSize, keepAliveTime, unit, queueCapacity, 46 | Executors.defaultThreadFactory(), defaultRejectedExecutionHandler 47 | ); 48 | } 49 | 50 | public static ThreadPoolExecutor newEagerThreadPool( 51 | int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, 52 | int queueCapacity, ThreadFactory threadFactory 53 | ) { 54 | return newEagerThreadPool( 55 | corePoolSize, maximumPoolSize, keepAliveTime, unit, queueCapacity, 56 | threadFactory, defaultRejectedExecutionHandler 57 | ); 58 | } 59 | 60 | public static ThreadPoolExecutor newEagerThreadPool( 61 | int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, 62 | int queueCapacity, RejectedExecutionHandler handler 63 | ) { 64 | return newEagerThreadPool( 65 | corePoolSize, maximumPoolSize, keepAliveTime, unit, queueCapacity, 66 | Executors.defaultThreadFactory(), handler 67 | ); 68 | } 69 | 70 | public static ThreadPoolExecutor newEagerThreadPool( 71 | int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, 72 | int queueCapacity, ThreadFactory threadFactory, RejectedExecutionHandler handler 73 | ) { 74 | NewThreadFirstQueue queue = new NewThreadFirstQueue<>(queueCapacity); 75 | ThreadPoolExecutor executor = new ThreadPoolExecutor( 76 | corePoolSize, maximumPoolSize, keepAliveTime, unit, queue, 77 | threadFactory, handler 78 | ); 79 | queue.setExecutor(executor); 80 | return executor; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /honoka-utils/src/main/java/de/honoka/sdk/util/file/AbstractEnvironmentPathUtils.java: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.util.file; 2 | 3 | import java.io.File; 4 | import java.nio.file.Paths; 5 | import java.util.Objects; 6 | 7 | public abstract class AbstractEnvironmentPathUtils { 8 | 9 | public enum BuildTool { 10 | 11 | MAVEN, GRADLE 12 | } 13 | 14 | private final BuildTool buildTool; 15 | 16 | public AbstractEnvironmentPathUtils(BuildTool buildTool) { 17 | this.buildTool = buildTool; 18 | } 19 | 20 | public String getDataDirPathOfApp() { 21 | String mainClasspath = FileUtils.getMainClasspath(); 22 | if(FileUtils.isAppRunningInJar()) return mainClasspath; 23 | switch(buildTool) { 24 | //大括号用于防止在不同的case块当中,由于变量名相同而产生的冲突 25 | case MAVEN: { 26 | File classesDir = Paths.get(mainClasspath, "..").normalize().toFile(); 27 | if(!Objects.equals(classesDir.getName(), "classes")) { 28 | throw new RuntimeException("Not normal maven classes directory: " + classesDir.getAbsolutePath()); 29 | } 30 | return Paths.get(classesDir.getAbsolutePath(), "../data").normalize().toString(); 31 | } 32 | case GRADLE: { 33 | File classesDir = Paths.get(mainClasspath, "../..").normalize().toFile(); 34 | if(!Objects.equals(classesDir.getName(), "classes")) { 35 | throw new RuntimeException("Not normal gradle classes directory: " + classesDir.getAbsolutePath()); 36 | } 37 | return Paths.get(classesDir.getAbsolutePath(), "../data").normalize().toString(); 38 | } 39 | default: 40 | throw new RuntimeException("Unknown build tool: " + buildTool); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /honoka-utils/src/main/java/de/honoka/sdk/util/file/FileUtils.java: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.util.file; 2 | 3 | import cn.hutool.core.collection.CollUtil; 4 | import cn.hutool.core.io.FileUtil; 5 | import cn.hutool.core.io.IoUtil; 6 | import lombok.SneakyThrows; 7 | 8 | import java.io.File; 9 | import java.net.URL; 10 | import java.net.URLDecoder; 11 | import java.nio.charset.StandardCharsets; 12 | import java.nio.file.Files; 13 | import java.nio.file.LinkOption; 14 | import java.nio.file.Path; 15 | import java.nio.file.Paths; 16 | import java.nio.file.attribute.BasicFileAttributeView; 17 | import java.nio.file.attribute.BasicFileAttributes; 18 | import java.util.Date; 19 | import java.util.List; 20 | import java.util.Locale; 21 | import java.util.Objects; 22 | import java.util.stream.Collectors; 23 | 24 | public class FileUtils { 25 | 26 | private static volatile String MAIN_CLASSPATH; 27 | 28 | /** 29 | * 检查当前运行的jar包外部是否含有指定的资源文件,若有则忽略此资源,若没有 30 | * 则从jar包中指定的相对路径处,提取此资源复制到jar包外部相同的相对路径处 31 | * 32 | * @param clazz 要提取资源的jar包中的某个类,用于基于此类进行相对路径的定位 33 | * @param paths 要提取的资源相对于clazz类所在路径的相对路径,以及要提取的 34 | * 资源所存放的位置相对于当前运行的jar包的相对路径 35 | * @return 指定的资源当中是否有某些资源原本不在jar包外部 36 | */ 37 | @SneakyThrows 38 | public static boolean copyResourceIfNotExists(Class clazz, String... paths) { 39 | if(!isAppRunningInJar()) return false; 40 | boolean result = false; 41 | String mainClasspath = getMainClasspath(); 42 | for(String path : paths) { 43 | URL url = clazz.getResource(path); 44 | if(url == null) continue; 45 | File file = new File(Paths.get(mainClasspath, path).toString()); 46 | if(file.exists()) continue; 47 | //指定的资源不存在 48 | result = true; 49 | FileUtil.touch(file); 50 | FileUtil.writeFromStream(url.openStream(), file); 51 | } 52 | return result; 53 | } 54 | 55 | public static Date getCreateTime(String filePath) { 56 | return getCreateTime(new File(filePath)); 57 | } 58 | 59 | public static Date getCreateTime(File file) { 60 | try { 61 | Path path = Paths.get(file.getAbsolutePath()); 62 | BasicFileAttributeView basicView = Files.getFileAttributeView( 63 | path, BasicFileAttributeView.class, 64 | LinkOption.NOFOLLOW_LINKS); 65 | BasicFileAttributes attr = basicView.readAttributes(); 66 | return new Date(attr.creationTime().toMillis()); 67 | } catch(Exception e) { 68 | e.printStackTrace(); 69 | return new Date(file.lastModified()); 70 | } 71 | } 72 | 73 | public static boolean isAppRunningInJar() { 74 | URL rootResourceUrl = Thread.currentThread().getContextClassLoader().getResource(""); 75 | if(rootResourceUrl == null) { 76 | throw new RuntimeException("Failed to get root resource"); 77 | } 78 | return Objects.equals(rootResourceUrl.getProtocol().toLowerCase(Locale.ROOT), "jar"); 79 | } 80 | 81 | /** 82 | * 获取当前运行环境的主classpath的绝对路径 83 | *

84 | * 当Java应用程序在jar包中被运行时,此路径为jar包所在目录的路径。在IDE中直接运行时,此路径为 85 | * 项目构建目录中的java源代码编译输出路径(如Maven中为“[项目目录]/target/classes”)。 86 | */ 87 | @SneakyThrows 88 | public static String getMainClasspath() { 89 | if(MAIN_CLASSPATH != null) return MAIN_CLASSPATH; 90 | URL rootResourceUrl = Thread.currentThread().getContextClassLoader().getResource(""); 91 | if(isAppRunningInJar()) { 92 | String path = Objects.requireNonNull(rootResourceUrl).getPath(); 93 | String pathEndSymbol; 94 | if(path.startsWith("file:/")) { 95 | pathEndSymbol = ".jar!/"; 96 | } else if(path.startsWith("nested:/")) { 97 | pathEndSymbol = ".jar/!"; 98 | } else { 99 | throw new RuntimeException("Root resource path is invalid: " + path); 100 | } 101 | int lowercaseSymbolIndex = path.indexOf(pathEndSymbol); 102 | int uppercaseSymbolIndex = path.indexOf(pathEndSymbol.toUpperCase(Locale.ROOT)); 103 | List symbolIndexes = CollUtil.newHashSet(lowercaseSymbolIndex, uppercaseSymbolIndex) 104 | .stream() 105 | .filter(it -> it != -1) 106 | .sorted() 107 | .collect(Collectors.toList()); 108 | if(symbolIndexes.isEmpty()) { 109 | throw new RuntimeException("Root resource path is invalid: " + path); 110 | } 111 | int pathStartIndex = 0; 112 | if(path.startsWith("file:/")) { 113 | pathStartIndex = 6; 114 | } else if(path.startsWith("nested:/")) { 115 | pathStartIndex = 8; 116 | } 117 | path = path.substring(pathStartIndex, symbolIndexes.get(0) + 4); 118 | path = path.substring(0, path.lastIndexOf("/")); 119 | path = URLDecoder.decode(path, StandardCharsets.UTF_8.name()); 120 | String result = Paths.get(path).normalize().toString(); 121 | String osName = System.getProperty("os.name").toLowerCase(Locale.ROOT); 122 | if(!osName.contains("windows") && !result.startsWith("/")) { 123 | result = "/" + result; 124 | } 125 | File dir = new File(result); 126 | if(!dir.exists() || !dir.isDirectory()) { 127 | throw new RuntimeException("Calculated main classpath is invalid: " + result); 128 | } 129 | MAIN_CLASSPATH = result; 130 | } else { 131 | MAIN_CLASSPATH = new File(Objects.requireNonNull(rootResourceUrl).toURI()).getAbsolutePath(); 132 | } 133 | return MAIN_CLASSPATH; 134 | } 135 | 136 | @SneakyThrows 137 | public static void checkOrMkdirs(File... dirs) { 138 | for(File dir : dirs) { 139 | if(!dir.exists()) dir.mkdirs(); 140 | } 141 | } 142 | 143 | /** 144 | * 检查必要的文件是否存在,不存在则创建 145 | */ 146 | @SneakyThrows 147 | public static void checkOrTouch(File... files) { 148 | for(File f : files) { 149 | FileUtil.touch(f); 150 | } 151 | } 152 | 153 | @SneakyThrows 154 | public static String fetchUrlResourceAndToString(URL url) { 155 | return new String(IoUtil.readBytes(url.openStream())); 156 | } 157 | 158 | public static String toUriPath(String filePath) { 159 | String uriPath = new File(filePath).toURI().toASCIIString(); 160 | if(uriPath.startsWith("file:/") && !uriPath.startsWith("file:///")) { 161 | uriPath = uriPath.replaceFirst("file:/", "file:///"); 162 | } 163 | return uriPath; 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /honoka-utils/src/main/java/de/honoka/sdk/util/file/csv/CsvTable.java: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.util.file.csv; 2 | 3 | import cn.hutool.core.io.FileUtil; 4 | import cn.hutool.core.io.IoUtil; 5 | import cn.hutool.core.util.StrUtil; 6 | import de.honoka.sdk.util.basic.ActionUtils; 7 | import de.honoka.sdk.util.various.ReflectUtils; 8 | import lombok.Getter; 9 | import lombok.SneakyThrows; 10 | import org.jetbrains.annotations.NotNull; 11 | 12 | import java.io.File; 13 | import java.io.InputStream; 14 | import java.lang.reflect.Field; 15 | import java.net.URL; 16 | import java.nio.charset.StandardCharsets; 17 | import java.util.*; 18 | 19 | /** 20 | * 将csv格式的表格加载为便于使用的对象 21 | */ 22 | public class CsvTable implements Iterable { 23 | 24 | @Getter 25 | private final List> rows = new ArrayList<>(); 26 | 27 | private Class dataType; 28 | 29 | public CsvTable(String csvText, Class dataType) { 30 | setDataType(dataType); 31 | loadTable(csvText); 32 | } 33 | 34 | @SneakyThrows 35 | public CsvTable(File f, Class dataType) { 36 | setDataType(dataType); 37 | loadTable(FileUtil.readString(f, StandardCharsets.UTF_8)); 38 | } 39 | 40 | @SneakyThrows 41 | public CsvTable(InputStream is, Class dataType) { 42 | setDataType(dataType); 43 | loadByInputStream(is); 44 | } 45 | 46 | @SneakyThrows 47 | public CsvTable(URL url, Class dataType) { 48 | setDataType(dataType); 49 | try(InputStream is = url.openConnection().getInputStream()) { 50 | loadByInputStream(is); 51 | } 52 | } 53 | 54 | public void setDataType(Class dataType) { 55 | if(dataType != null && Map.class.isAssignableFrom(dataType)) { 56 | throw new IllegalArgumentException("Please use DefaultCsvTable."); 57 | } 58 | this.dataType = dataType; 59 | } 60 | 61 | //该方法不关闭流 62 | @SneakyThrows 63 | private void loadByInputStream(InputStream is) { 64 | loadTable(new String(IoUtil.readBytes(is, false), StandardCharsets.UTF_8)); 65 | } 66 | 67 | private void loadTable(String csvText) { 68 | csvText = csvText.trim(); 69 | String[] rows = csvText.split("\n"); 70 | if(rows.length < 2) return; 71 | String[] headers = rows[0].trim().split(","); 72 | for(int i = 1; i < rows.length; i++) { 73 | String rowStr = rows[i].trim(); 74 | String[] cols = rowStr.split(","); 75 | Map row = new HashMap<>(); 76 | for(int j = 0; j < headers.length; j++) { 77 | try { 78 | row.put(headers[j].trim(), cols[j].trim()); 79 | } catch(Throwable t) { 80 | row.put(headers[j].trim(), null); 81 | } 82 | } 83 | this.rows.add(row); 84 | } 85 | } 86 | 87 | @SneakyThrows 88 | protected T rowToBean(Map row) { 89 | T bean = ReflectUtils.newInstance(dataType); 90 | for(Field field : dataType.getDeclaredFields()) { 91 | field.setAccessible(true); 92 | String fieldName = field.getName(); 93 | Class fieldType = field.getType(); 94 | String value = row.get(fieldName); 95 | //表格中是空串,一律视为null值 96 | if(StrUtil.isEmpty(value)) { 97 | ActionUtils.doIgnoreException(() -> field.set(bean, null)); 98 | continue; 99 | } 100 | switch(fieldType.getSimpleName()) { 101 | case "int": 102 | case "Integer": 103 | field.set(bean, Integer.parseInt(value)); 104 | break; 105 | case "long": 106 | case "Long": 107 | field.set(bean, Long.parseLong(value)); 108 | break; 109 | case "double": 110 | case "Double": 111 | field.set(bean, Double.parseDouble(value)); 112 | break; 113 | case "boolean": 114 | case "Boolean": 115 | field.set(bean, Boolean.parseBoolean(value)); 116 | break; 117 | case "String": 118 | field.set(bean, value); 119 | break; 120 | default: 121 | throw new UnsupportedOperationException("Unknown field " + 122 | "type in " + dataType.getName() + ": " + fieldName + 123 | "(" + fieldType.getName() + ")"); 124 | } 125 | } 126 | return bean; 127 | } 128 | 129 | @Override 130 | public @NotNull Iterator iterator() { 131 | Iterator> realIterator = rows.iterator(); 132 | return new Iterator() { 133 | 134 | @Override 135 | public boolean hasNext() { 136 | return realIterator.hasNext(); 137 | } 138 | 139 | @Override 140 | public T next() { 141 | return rowToBean(realIterator.next()); 142 | } 143 | }; 144 | } 145 | 146 | /** 147 | * 遍历表格,查找第一个符合条件的行(header列为value值的行),未找到返回null 148 | */ 149 | public T find(String header, String value) { 150 | for(Map row : rows) { 151 | if(row.get(header).equals(value)) return rowToBean(row); 152 | } 153 | return null; 154 | } 155 | 156 | public List findAll(String header, String value) { 157 | List list = new ArrayList<>(); 158 | for(Map row : rows) { 159 | if(row.get(header).equals(value)) list.add(rowToBean(row)); 160 | } 161 | return list; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /honoka-utils/src/main/java/de/honoka/sdk/util/file/csv/DefaultCsvTable.java: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.util.file.csv; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import java.util.Iterator; 6 | import java.util.Map; 7 | 8 | public class DefaultCsvTable extends CsvTable> { 9 | 10 | public DefaultCsvTable(String csvText) { 11 | super(csvText, null); 12 | } 13 | 14 | @Override 15 | protected Map rowToBean(Map row) { 16 | return row; 17 | } 18 | 19 | @Override 20 | public @NotNull Iterator> iterator() { 21 | return getRows().iterator(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /honoka-utils/src/main/java/de/honoka/sdk/util/gui/ColorAttributeSets.java: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.util.gui; 2 | 3 | import javax.swing.text.AttributeSet; 4 | import javax.swing.text.SimpleAttributeSet; 5 | import javax.swing.text.StyleConstants; 6 | import java.awt.*; 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | public class ColorAttributeSets { 11 | 12 | public static final int MIN_CODE = 30, MAX_CODE = 39; 13 | 14 | private static final List sets = new ArrayList<>(); 15 | 16 | private static final List colors = new ArrayList<>(); 17 | 18 | static { 19 | init(); 20 | } 21 | 22 | private static void init() { 23 | color(255, 255, 255); //30 24 | color(255, 107, 105); //31 25 | color(168, 192, 35); //32 26 | color(213, 191, 86); //33 27 | color(83, 147, 236); //34 28 | color(173, 138, 190); //35 29 | color(40, 153, 153); //36 30 | color(153, 153, 153); //37 31 | color(187, 187, 187); //38 32 | color(187, 187, 187); //39 33 | } 34 | 35 | private static void color(int r, int g, int b) { 36 | Color color = new Color(r, g, b, 255); 37 | colors.add(color); 38 | SimpleAttributeSet attributeSet = new SimpleAttributeSet(); 39 | StyleConstants.setForeground(attributeSet, color); 40 | sets.add(attributeSet); 41 | } 42 | 43 | public static AttributeSet getAttributeSet(int ansiCode) { 44 | if(ansiCode < MIN_CODE || ansiCode > MAX_CODE) return null; 45 | return sets.get(ansiCode - 30); 46 | } 47 | 48 | public static Color getColor(int ansiCode) { 49 | if(ansiCode < MIN_CODE || ansiCode > MAX_CODE) return null; 50 | return colors.get(ansiCode - 30); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /honoka-utils/src/main/java/de/honoka/sdk/util/gui/ColorfulOutputStream.java: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.util.gui; 2 | 3 | import cn.hutool.core.util.ArrayUtil; 4 | import de.honoka.sdk.util.basic.ActionUtils; 5 | import lombok.Getter; 6 | import lombok.Setter; 7 | import lombok.SneakyThrows; 8 | 9 | import java.awt.*; 10 | import java.io.OutputStream; 11 | import java.io.PrintStream; 12 | import java.util.ArrayList; 13 | import java.util.Arrays; 14 | import java.util.List; 15 | import java.util.function.Consumer; 16 | 17 | public class ColorfulOutputStream extends OutputStream { 18 | 19 | private final List buffer = new ArrayList<>(); 20 | 21 | protected final PrintStream originalPrintStream; 22 | 23 | private final Color defaultPrintColor; 24 | 25 | @Getter 26 | @Setter 27 | private Color printColor; 28 | 29 | @Setter 30 | private Consumer printMethod; 31 | 32 | public ColorfulOutputStream(PrintStream originalPrintStream, Color defaultPrintColor) { 33 | this.originalPrintStream = originalPrintStream; 34 | this.defaultPrintColor = defaultPrintColor; 35 | printColor = defaultPrintColor; 36 | } 37 | 38 | @Override 39 | public void write(int b) { 40 | buffer.add((byte) b); 41 | originalPrintStream.write(b); 42 | } 43 | 44 | @SuppressWarnings("NullableProblems") 45 | @SneakyThrows 46 | @Override 47 | public synchronized void write(byte[] b, int off, int len) { 48 | super.write(b, off, len); 49 | //这里在字节数组输出完成后将缓存中的字节传递给消费者 50 | printMethod.accept(ArrayUtil.unWrap(buffer.toArray(new Byte[0]))); 51 | buffer.clear(); 52 | } 53 | 54 | public String changePrintColorByAnsiString(String str) { 55 | if(!str.startsWith("[") || !str.contains("m")) return str; 56 | String ansi = str.substring(str.indexOf("[") + 1, str.indexOf("m")); 57 | List parts = Arrays.asList(ansi.split(";")); 58 | parts.forEach(s -> { 59 | if(s.equals("0")) { 60 | printColor = defaultPrintColor; 61 | return; 62 | } 63 | ActionUtils.doIgnoreException(() -> { 64 | int ansiCode = Integer.parseInt(s); 65 | Color color = ColorAttributeSets.getColor(ansiCode); 66 | if(color != null) { 67 | printColor = color; 68 | } 69 | }); 70 | }); 71 | return str.substring(str.indexOf("m") + 1); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /honoka-utils/src/main/java/de/honoka/sdk/util/gui/ConsoleInputStream.java: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.util.gui; 2 | 3 | import lombok.SneakyThrows; 4 | 5 | import java.io.InputStream; 6 | import java.util.LinkedList; 7 | import java.util.Queue; 8 | 9 | public class ConsoleInputStream extends InputStream { 10 | 11 | //package-private 12 | final Queue buffer = new LinkedList<>(); 13 | 14 | private final ConsoleWindow window; 15 | 16 | public ConsoleInputStream(ConsoleWindow window) { 17 | this.window = window; 18 | } 19 | 20 | @SneakyThrows 21 | @Override 22 | public int read() { 23 | Byte b = buffer.poll(); 24 | if(b != null) return b.intValue(); 25 | while(b == null) { 26 | synchronized(this) { 27 | window.showInputField(); 28 | this.wait(); 29 | b = buffer.poll(); 30 | } 31 | } 32 | window.hideInputField(); 33 | return b.intValue(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /honoka-utils/src/main/java/de/honoka/sdk/util/gui/ConsoleOutputStream.java: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.util.gui; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import lombok.SneakyThrows; 6 | 7 | import javax.swing.text.AttributeSet; 8 | import javax.swing.text.SimpleAttributeSet; 9 | import javax.swing.text.StyleConstants; 10 | import java.awt.*; 11 | import java.io.OutputStream; 12 | import java.io.PrintStream; 13 | import java.util.ArrayList; 14 | import java.util.Arrays; 15 | import java.util.List; 16 | import java.util.function.Consumer; 17 | 18 | /** 19 | * 用于代理系统输出流的自定义输出流 20 | */ 21 | public class ConsoleOutputStream extends OutputStream { 22 | 23 | private final List buffer = new ArrayList<>(); 24 | 25 | protected final PrintStream originalPrintStream; 26 | 27 | private final AttributeSet defaultPrintAttributeSet; 28 | 29 | @Getter 30 | @Setter 31 | private AttributeSet printAttributeSet; 32 | 33 | @Setter 34 | private Consumer> printMethod; 35 | 36 | public ConsoleOutputStream(PrintStream systemPrintStream, Color printColor) { 37 | this.originalPrintStream = systemPrintStream; 38 | SimpleAttributeSet attributeSet = new SimpleAttributeSet(); 39 | StyleConstants.setForeground(attributeSet, printColor); 40 | defaultPrintAttributeSet = attributeSet; 41 | printAttributeSet = attributeSet; 42 | } 43 | 44 | @Override 45 | public void write(int b) { 46 | buffer.add((byte) b); 47 | originalPrintStream.write(b); 48 | } 49 | 50 | @SuppressWarnings("NullableProblems") 51 | @SneakyThrows 52 | @Override 53 | public synchronized void write(byte[] b, int off, int len) { 54 | super.write(b, off, len); 55 | //这里在字节数组输出完成后将缓存中的字节转换为字符串然后输出 56 | printMethod.accept(buffer); 57 | buffer.clear(); 58 | } 59 | 60 | public String changePrintColorByAnsiString(String str) { 61 | if(!str.startsWith("[") || !str.contains("m")) return str; 62 | String ansi = str.substring(str.indexOf("[") + 1, str.indexOf("m")); 63 | List parts = Arrays.asList(ansi.split(";")); 64 | parts.forEach(s -> { 65 | if(s.equals("0")) { 66 | setPrintAttributeSet(defaultPrintAttributeSet); 67 | return; 68 | } 69 | try { 70 | int ansiCode = Integer.parseInt(s); 71 | AttributeSet attributeSet = ColorAttributeSets 72 | .getAttributeSet(ansiCode); 73 | if(attributeSet != null) setPrintAttributeSet(attributeSet); 74 | } catch(Exception e) { 75 | //none 76 | } 77 | }); 78 | return str.substring(str.indexOf("m") + 1); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /honoka-utils/src/main/java/de/honoka/sdk/util/gui/ConsoleWindowBuilder.java: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.util.gui; 2 | 3 | import de.honoka.sdk.util.basic.ThrowsRunnable; 4 | import lombok.AccessLevel; 5 | import lombok.Getter; 6 | import lombok.Setter; 7 | import lombok.experimental.Accessors; 8 | 9 | import java.awt.*; 10 | import java.net.URL; 11 | 12 | /** 13 | * ConsoleWindow初始化时的默认参数 14 | */ 15 | @Getter 16 | @Setter 17 | @Accessors(chain = true) 18 | public class ConsoleWindowBuilder { 19 | 20 | private String windowName = "Untitled"; 21 | 22 | private boolean showOnBuild = true; 23 | 24 | private double screenZoomScale = 1.0; 25 | 26 | private int windowWidth = 1000; 27 | 28 | private int windowHeight = 600; 29 | 30 | private String menuItemFontName = "Microsoft YaHei UI"; 31 | 32 | private int menuItemFontStyle = Font.PLAIN; 33 | 34 | private int menuItemFontSize = 12; 35 | 36 | private String textPaneFontName = "Microsoft YaHei Mono"; 37 | 38 | private int textPaneFontStyle = Font.PLAIN; 39 | 40 | private int textPaneFontSize = 18; 41 | 42 | private int textPaneMaxLine = 200; 43 | 44 | @Getter(AccessLevel.NONE) 45 | private final Dimension defaultTrayIconMenuLocationOffset = new Dimension(21, 17); 46 | 47 | @Setter(AccessLevel.NONE) 48 | private Dimension trayIconMenuLocationOffset = new Dimension( 49 | defaultTrayIconMenuLocationOffset.width, 50 | defaultTrayIconMenuLocationOffset.height 51 | ); 52 | 53 | /** 54 | * 是否需要创建托盘图标 55 | */ 56 | private boolean backgroundMode = false; 57 | 58 | private URL trayIconPath; 59 | 60 | private ThrowsRunnable onExit; 61 | 62 | private ConsoleWindowBuilder() {} 63 | 64 | //package-private 65 | static ConsoleWindowBuilder of() { 66 | return new ConsoleWindowBuilder(); 67 | } 68 | 69 | //package-private 70 | static ConsoleWindowBuilder of(String windowName) { 71 | return new ConsoleWindowBuilder().setWindowName(windowName); 72 | } 73 | 74 | /** 75 | * width:为负表示左移,为正表示右移 76 | * height:为负表示上移,为正表示下移 77 | */ 78 | public ConsoleWindowBuilder setTrayIconMenuLocationOffset(int width, int height) { 79 | width = defaultTrayIconMenuLocationOffset.width - width; 80 | height = defaultTrayIconMenuLocationOffset.height - height; 81 | trayIconMenuLocationOffset = new Dimension(width, height); 82 | return this; 83 | } 84 | 85 | public ConsoleWindow build() { 86 | //注入参数 87 | ConsoleWindow consoleWindow = new ConsoleWindow(); 88 | consoleWindow.windowName = windowName; 89 | consoleWindow.screenZoomScale = screenZoomScale; 90 | consoleWindow.defaultFrameSize = new Dimension(windowWidth, windowHeight); 91 | consoleWindow.menuItemFont = new Font(menuItemFontName, 92 | menuItemFontStyle, menuItemFontSize); 93 | consoleWindow.textPaneFont = new Font(textPaneFontName, 94 | textPaneFontStyle, textPaneFontSize); 95 | consoleWindow.textPaneMaxLine = textPaneMaxLine; 96 | consoleWindow.trayIconMenuLocationOffset = trayIconMenuLocationOffset; 97 | //构建 98 | if(backgroundMode) { 99 | consoleWindow.initBackgroundMode(trayIconPath, onExit); 100 | if(showOnBuild) consoleWindow.show(); 101 | } else { 102 | if(onExit == null) consoleWindow.init(); 103 | else consoleWindow.init(onExit); 104 | consoleWindow.show(); 105 | } 106 | return consoleWindow; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /honoka-utils/src/main/java/de/honoka/sdk/util/gui/WrapableJTextPane.java: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.util.gui; 2 | 3 | import javax.swing.*; 4 | import javax.swing.text.*; 5 | 6 | /** 7 | * 该类是真正实现超长单词都能自动换行的JTextPane的子类。 8 | *

9 | * Java 7以下版本的JTextPane本身都能实现自动换行,对超长单词都能有效, 10 | * 但从Java 7开始,超长单词就不能自动换行,导致JTextPane的实际宽度变大, 11 | * 使得滚动条出现。下面的方法是对这个bug的较好修复。 12 | */ 13 | public class WrapableJTextPane extends JTextPane { 14 | 15 | //以下内部类全都用于实现自动强制折行 16 | 17 | private static class WarpEditorKit extends StyledEditorKit { 18 | 19 | private final ViewFactory defaultFactory = new WarpColumnFactory(); 20 | 21 | @Override 22 | public ViewFactory getViewFactory() { 23 | return defaultFactory; 24 | } 25 | } 26 | 27 | private static class WarpColumnFactory implements ViewFactory { 28 | 29 | public View create(Element elem) { 30 | String kind = elem.getName(); 31 | if(kind != null) { 32 | switch(kind) { 33 | case AbstractDocument.ContentElementName: 34 | return new WarpLabelView(elem); 35 | case AbstractDocument.ParagraphElementName: 36 | return new ParagraphView(elem); 37 | case AbstractDocument.SectionElementName: 38 | return new BoxView(elem, View.Y_AXIS); 39 | case StyleConstants.ComponentElementName: 40 | return new ComponentView(elem); 41 | case StyleConstants.IconElementName: 42 | return new IconView(elem); 43 | } 44 | } 45 | 46 | // default to text display 47 | return new LabelView(elem); 48 | } 49 | } 50 | 51 | private static class WarpLabelView extends LabelView { 52 | 53 | public WarpLabelView(Element elem) { 54 | super(elem); 55 | } 56 | 57 | @Override 58 | public float getMinimumSpan(int axis) { 59 | switch(axis) { 60 | case View.X_AXIS: 61 | return 0; 62 | case View.Y_AXIS: 63 | return super.getMinimumSpan(axis); 64 | default: 65 | throw new IllegalArgumentException("Invalid axis: " + axis); 66 | } 67 | } 68 | } 69 | 70 | public WrapableJTextPane() { 71 | super(); 72 | this.setEditorKit(new WarpEditorKit()); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /honoka-utils/src/main/java/de/honoka/sdk/util/text/EmojiHelper.java: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.util.text; 2 | 3 | import java.io.UnsupportedEncodingException; 4 | 5 | /** 6 | * emoji字符处理类,只适用于由两个字符组成的emoji字符 7 | */ 8 | public class EmojiHelper { 9 | 10 | /** 11 | * 将unicode码转为emoji字符 12 | * 13 | * @param unicode unicode码 14 | * @return emoji字符 15 | */ 16 | public static String unicodeToEmoji(int unicode) { 17 | String emoji; 18 | StringBuilder hex = new StringBuilder(Integer.toHexString(unicode)); 19 | while(hex.length() < 8) { 20 | hex.insert(0, "0"); 21 | } 22 | char[] chars = hex.toString().toCharArray(); 23 | byte[] bytes = new byte[4]; 24 | for(int i = 0; i < chars.length; i += 2) { 25 | bytes[i / 2] = (byte) (Integer.parseInt("" + chars[i] + chars[i + 1], 16)); 26 | } 27 | try { 28 | emoji = new String(bytes, "utf-32"); 29 | } catch(UnsupportedEncodingException e) { 30 | emoji = ""; 31 | } 32 | return emoji; 33 | } 34 | 35 | /** 36 | * 将emoji字符转为unicode码 37 | * 38 | * @param emoji emoji字符 39 | * @return unicode码 40 | */ 41 | public static int emojiToUnicode(String emoji) { 42 | int unicode; 43 | try { 44 | byte[] bytes = emoji.getBytes("utf-32"); 45 | StringBuilder sb = new StringBuilder(); 46 | String temp; 47 | for(byte aByte : bytes) { 48 | temp = Integer.toHexString(aByte & 0xFF); 49 | if(temp.length() == 1) { 50 | //1得到一位的进行补0操作 51 | sb.append("0"); 52 | } 53 | sb.append(temp); 54 | } 55 | unicode = Integer.parseInt(sb.toString(), 16); 56 | } catch(Exception e) { 57 | return 0; 58 | } 59 | return unicode; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /honoka-utils/src/main/java/de/honoka/sdk/util/text/EncodingUtils.java: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.util.text; 2 | 3 | import lombok.SneakyThrows; 4 | 5 | import java.net.URLDecoder; 6 | import java.net.URLEncoder; 7 | 8 | /** 9 | * 用于编码、解码URL中的专有字符 10 | */ 11 | public class EncodingUtils { 12 | 13 | /** 14 | * Decodes the passed UTF-8 String using an algorithm that's 15 | * compatible with JavaScript's decodeURIComponent 16 | * function. Returns null if the String is null. 17 | * 18 | * @param s The UTF-8 encoded String to be decoded 19 | * @return the decoded String 20 | */ 21 | @SneakyThrows 22 | public static String decodeURIComponent(String s) { 23 | if(s == null) { 24 | return null; 25 | } 26 | String result; 27 | result = URLDecoder.decode(s, "UTF-8"); 28 | return result; 29 | } 30 | 31 | /** 32 | * Encodes the passed String as UTF-8 using an algorithm that's 33 | * compatible with JavaScript's encodeURIComponent 34 | * function. Returns null if the String is null. 35 | * 36 | * @param s The String to be encoded 37 | * @return the encoded String 38 | */ 39 | @SneakyThrows 40 | public static String encodeURIComponent(String s) { 41 | String result; 42 | result = URLEncoder.encode(s, "UTF-8") 43 | .replaceAll("\\+", "%20") 44 | .replaceAll("%21", "!") 45 | .replaceAll("%27", "'") 46 | .replaceAll("%28", "(") 47 | .replaceAll("%29", ")") 48 | .replaceAll("%7E", "~"); 49 | return result; 50 | } 51 | 52 | /** 53 | * Private constructor to prevent this class from being instantiated. 54 | */ 55 | private EncodingUtils() { 56 | super(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /honoka-utils/src/main/java/de/honoka/sdk/util/text/HtmlUtils.java: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.util.text; 2 | 3 | import cn.hutool.core.util.EscapeUtil; 4 | 5 | public class HtmlUtils { 6 | 7 | /** 8 | * 由于html文档中的“<>”符号都被转义,文本内容中不可能出现这个符号,所以可以根据 9 | * 这两个符号检查字符串中的html标签,并加以清除。同时将html中的转义符号还原。 10 | * 11 | * @param html html文档字符串 12 | * @return 清除后的文本 13 | */ 14 | public static String clearHtmlTags(String html) { 15 | //由于文档字符串可能存在缩进空格,为了不让其影响文本,需要予以清除 16 | String[] htmlRows = html.split("\n"); //将文档字符串分为每一行 17 | StringBuilder htmlBuilder = new StringBuilder(); 18 | for(String htmlRow : htmlRows) { 19 | //去除每一行左右两边的缩进空格后拼接为一个字符串 20 | htmlBuilder.append(htmlRow.trim()).append("\n"); 21 | } 22 | html = htmlBuilder.toString(); 23 | //println(html); 24 | while(html.contains("<") && html.contains(">")) { 25 | //检索html标签 26 | int leftSymbolIndex = html.indexOf("<"), rightSymbolIndex = html.indexOf(">"); 27 | //要清除的html标签 28 | String tag = html.substring(leftSymbolIndex, rightSymbolIndex + 1); 29 | //println(tag); 30 | //将文档中与此标签匹配的标签清除 31 | html = html.replace(tag, ""); 32 | } 33 | //解码最终结果中的html转义字符 34 | html = EscapeUtil.unescapeHtml4(html); 35 | return html; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /honoka-utils/src/main/java/de/honoka/sdk/util/text/TextUtils.java: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.util.text; 2 | 3 | import java.text.DateFormat; 4 | import java.text.SimpleDateFormat; 5 | import java.util.Arrays; 6 | import java.util.Collection; 7 | import java.util.Iterator; 8 | import java.util.List; 9 | 10 | public class TextUtils { 11 | 12 | /** 13 | * 将字符串集合转换为单行集合 14 | */ 15 | public static String toSingleLineString(Collection collection) { 16 | StringBuilder str = new StringBuilder(); 17 | Iterator itr = collection.iterator(); 18 | while(itr.hasNext()) { 19 | String item = itr.next(); 20 | if(itr.hasNext()) str.append(item).append(", "); 21 | else str.append(item); 22 | } 23 | return str.toString(); 24 | } 25 | 26 | public static DateFormat getSimpleDateFormat() { 27 | return getSimpleDateFormat("normal"); 28 | } 29 | 30 | /** 31 | * 获取一个常用的SimpleDateFormat 32 | */ 33 | public static DateFormat getSimpleDateFormat(String type) { 34 | type = type.toLowerCase(); 35 | switch(type) { 36 | case "chinese": 37 | return new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss"); 38 | case "normal": 39 | default: 40 | return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 41 | } 42 | } 43 | 44 | /** 45 | * 将bool值转换为“开”或“关” 46 | */ 47 | public static String boolSwitchToString(boolean b) { 48 | return b ? "开" : "关"; 49 | } 50 | 51 | /** 52 | * 获取每一行(供JDK 1.8及以下使用) 53 | */ 54 | public static List getLines(String s) { 55 | s = s.replace("\r", ""); 56 | return Arrays.asList(s.split("\n")); 57 | } 58 | 59 | /** 60 | * 获取字符串半角长度(全角字符算2个单位,半角字符算1个单位) 61 | */ 62 | public static int getHalfWidthLength(String s) { 63 | int count = 0; 64 | for(int i = 0; i < s.length(); i++) { 65 | char c = s.charAt(i); 66 | count += c < 0x800 ? 1 : 2; 67 | } 68 | return count; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /honoka-utils/src/main/java/de/honoka/sdk/util/text/XmlUtils.java: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.util.text; 2 | 3 | import lombok.SneakyThrows; 4 | import org.dom4j.Document; 5 | import org.dom4j.DocumentHelper; 6 | import org.dom4j.Element; 7 | import org.dom4j.io.SAXReader; 8 | import org.intellij.lang.annotations.Language; 9 | 10 | import java.io.StringReader; 11 | 12 | public class XmlUtils { 13 | 14 | @SneakyThrows 15 | public static Document read(@Language("XML") String xmlStr) { 16 | return new SAXReader().read(new StringReader(xmlStr)); 17 | } 18 | 19 | public static Element readRootElement(@Language("XML") String xmlStr) { 20 | return read(xmlStr).getRootElement(); 21 | } 22 | 23 | @SneakyThrows 24 | public static Element parseElement(@Language("XML") String str) { 25 | Element rootElement = DocumentHelper.parseText(str).getRootElement(); 26 | rootElement.setDocument(null); 27 | return rootElement; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /honoka-utils/src/main/java/de/honoka/sdk/util/various/ImageUtils.java: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.util.various; 2 | 3 | import de.honoka.sdk.util.file.FileUtils; 4 | import de.honoka.sdk.util.text.TextUtils; 5 | import gui.ava.html.Html2Image; 6 | import gui.ava.html.renderer.ImageRenderer; 7 | import lombok.SneakyThrows; 8 | 9 | import javax.imageio.ImageIO; 10 | import java.awt.image.BufferedImage; 11 | import java.io.ByteArrayInputStream; 12 | import java.io.ByteArrayOutputStream; 13 | import java.io.InputStream; 14 | import java.net.URL; 15 | 16 | public class ImageUtils { 17 | 18 | public static final int DEFAULT_IMAGE_WIDTH = 1000; 19 | 20 | @SneakyThrows 21 | public static InputStream htmlToImage(String html, int width) { 22 | ImageRenderer render = Html2Image.fromHtml(html).getImageRenderer().setWidth(width).setAutoHeight(true); 23 | BufferedImage img = render.getBufferedImage(); 24 | ByteArrayOutputStream bytes = new ByteArrayOutputStream(); 25 | ImageIO.write(img, "png", bytes); 26 | return new ByteArrayInputStream(bytes.toByteArray()); 27 | } 28 | 29 | @SneakyThrows 30 | private static String getTextImageHtml(String str) { 31 | URL textHtml = ImageUtils.class.getResource("/text.html"); 32 | if(textHtml == null) return null; 33 | str = "

" + str + "
"; 34 | str = str.replace("\t", " "); 35 | return String.format(FileUtils.fetchUrlResourceAndToString(textHtml), str); 36 | } 37 | 38 | /** 39 | * 文本强制换行,每行字数限制在一定范围内,超过则强制拆分为多行 40 | */ 41 | private static String forceWarp(String text, int lineLength) { 42 | //提取行 43 | String[] lines = text.split("\n"); 44 | StringBuilder textBuilder = new StringBuilder(); 45 | for(int i = 0; i < lines.length; i++) { 46 | String line = lines[i]; 47 | if(TextUtils.getHalfWidthLength(line) <= lineLength) { 48 | //行长度不超过限制 49 | textBuilder.append(line); 50 | } else { 51 | int halfWidthIndex = 0; 52 | for(int j = 0; j < line.length(); j++) { 53 | char c = line.charAt(j); 54 | textBuilder.append(c); 55 | halfWidthIndex += c < 0x800 ? 1 : 2; 56 | if(halfWidthIndex >= lineLength) { 57 | halfWidthIndex = 0; 58 | if(j < line.length() - 1) { 59 | textBuilder.append("\n"); 60 | } 61 | } 62 | } 63 | } 64 | //当前添加的行(不论是否被拆分)是否为最后一行 65 | if(i != lines.length - 1) textBuilder.append("\n"); 66 | } 67 | return textBuilder.toString(); 68 | } 69 | 70 | /** 71 | * 指定图片大小,不限制每行字符数 72 | */ 73 | public static InputStream textToImageBySize(String text, int imageSize) { 74 | return htmlToImage(getTextImageHtml(text), imageSize); 75 | } 76 | 77 | /** 78 | * 指定每行最大字符数(半角),自动计算图片大小。 79 | *

80 | * 文本图片默认图片大小 imageSize = lineLength * 28 81 | */ 82 | public static InputStream textToImageByLength(String text, int lineLength) { 83 | return htmlToImage(getTextImageHtml(forceWarp(text, lineLength)), lineLength * 28); 84 | } 85 | 86 | public static InputStream textToImage(String text) { 87 | return textToImageBySize(text, DEFAULT_IMAGE_WIDTH); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /honoka-utils/src/main/java/de/honoka/sdk/util/various/ListRunner.java: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.util.various; 2 | 3 | import de.honoka.sdk.util.basic.ThrowsRunnable; 4 | 5 | import java.util.ArrayList; 6 | import java.util.Collections; 7 | import java.util.List; 8 | 9 | /** 10 | * 列表运行器,可以接收很多个runnable,依次运行它们 11 | */ 12 | public class ListRunner { 13 | 14 | //此类的run方法将抛弃异常 15 | private static class Action implements Comparable { 16 | 17 | public ThrowsRunnable runnable; 18 | 19 | //优先级,默认为最低优先级 20 | public int priority = Integer.MAX_VALUE; 21 | 22 | public Action(ThrowsRunnable runnable) { 23 | this.runnable = runnable; 24 | } 25 | 26 | @Override 27 | public int compareTo(Action o) { 28 | //如果参数等于此实例,则返回值 0; 29 | //如果此实例小于参数,则返回一个小于 0 的值; 30 | //如果此实例大于参数,则返回一个大于 0 的值。 31 | if(o.priority == priority) return 0; 32 | return priority < o.priority ? -1 : 1; 33 | } 34 | 35 | public void run() throws Throwable { 36 | runnable.throwsRun(); 37 | } 38 | } 39 | 40 | private final List actions = new ArrayList<>(); 41 | 42 | //依次执行每个操作时是否要忽略异常 43 | private final boolean ignoreExceptions; 44 | 45 | public ListRunner(boolean ignoreExceptions) { 46 | this.ignoreExceptions = ignoreExceptions; 47 | } 48 | 49 | public void add(ThrowsRunnable runnable) { 50 | actions.add(new Action(runnable)); 51 | } 52 | 53 | public void add(ThrowsRunnable runnable, int priority) { 54 | Action action = new Action(runnable); 55 | action.priority = priority; 56 | actions.add(action); 57 | } 58 | 59 | //为便于使用,run方法不抛出异常,而是返回异常对象 60 | //返回值为null时表示运行时无异常 61 | public Throwable run() { 62 | //先对列表按优先级排序 63 | Collections.sort(actions); 64 | for(Action action : actions) { 65 | try { 66 | action.run(); 67 | } catch(Throwable t) { 68 | if(!ignoreExceptions) return t; 69 | } 70 | } 71 | return null; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /honoka-utils/src/main/java/de/honoka/sdk/util/various/ReflectUtils.java: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.util.various; 2 | 3 | import lombok.SneakyThrows; 4 | 5 | import java.lang.reflect.Constructor; 6 | import java.lang.reflect.Field; 7 | import java.lang.reflect.Method; 8 | import java.lang.reflect.Modifier; 9 | 10 | /** 11 | * 便于进行反射操作的工具类 12 | */ 13 | public class ReflectUtils { 14 | 15 | /** 16 | * 获取某个定义的字段,并调整可访问性,默认移除final修饰符(如果有) 17 | */ 18 | @SneakyThrows(NoSuchFieldException.class) 19 | public static Field getField(Class clazz, String fieldName) { 20 | Field f = clazz.getDeclaredField(fieldName); 21 | f.setAccessible(true); 22 | removeFinalModifier(f); 23 | return f; 24 | } 25 | 26 | /** 27 | * 从某个对象的类或父类中获取字段,并调整可访问性 28 | */ 29 | public static Field getField(Object obj, String fieldName) { 30 | Class clazz = obj.getClass(); 31 | for(; ; ) { 32 | try { 33 | return getField(clazz, fieldName); 34 | } catch(Exception e) { 35 | clazz = clazz.getSuperclass(); 36 | if(clazz == null) throw e; 37 | } 38 | } 39 | } 40 | 41 | /** 42 | * 获取某个定义的方法,并调整可访问性 43 | */ 44 | @SneakyThrows(NoSuchMethodException.class) 45 | public static Method getMethod(Class clazz, String methodName, Class... parameterTypes) { 46 | Method m = clazz.getDeclaredMethod(methodName, parameterTypes); 47 | m.setAccessible(true); 48 | return m; 49 | } 50 | 51 | /** 52 | * 从某个对象的类或父类中获取方法,并调整可访问性 53 | */ 54 | public static Method getMethod(Object obj, String methodName, Class... parameterTypes) { 55 | Class clazz = obj.getClass(); 56 | for(; ; ) { 57 | try { 58 | return getMethod(clazz, methodName, parameterTypes); 59 | } catch(Exception e) { 60 | clazz = clazz.getSuperclass(); 61 | if(clazz == null) throw e; 62 | } 63 | } 64 | } 65 | 66 | /** 67 | * 获取某个对象的一个成员的值 68 | */ 69 | @SneakyThrows 70 | public static Object getFieldValue(Object obj, String fieldName) { 71 | Field f = getField(obj, fieldName); 72 | return f.get(obj); 73 | } 74 | 75 | /** 76 | * 获取static字段的值 77 | */ 78 | @SneakyThrows 79 | public static Object getFieldValue(Class clazz, String fieldName) { 80 | Field f = getField(clazz, fieldName); 81 | return f.get(null); 82 | } 83 | 84 | /** 85 | * 设置某个对象的一个成员的值 86 | */ 87 | @SneakyThrows 88 | public static void setFieldValue(Object obj, String fieldName, Object value) { 89 | Field f = getField(obj, fieldName); 90 | f.set(obj, value); 91 | } 92 | 93 | /** 94 | * 设置static字段的值 95 | */ 96 | @SneakyThrows 97 | public static void setFieldValue(Class clazz, String fieldName, Object value) { 98 | Field f = getField(clazz, fieldName); 99 | f.set(null, value); 100 | } 101 | 102 | /** 103 | * 移除field上的final修饰符(如果有) 104 | */ 105 | @SneakyThrows 106 | private static void removeFinalModifier(Field f) { 107 | if(!Modifier.isFinal(f.getModifiers())) return; 108 | Field modifiersField = f.getClass().getDeclaredField("modifiers"); 109 | modifiersField.setAccessible(true); 110 | modifiersField.setInt(f, f.getModifiers() & ~Modifier.FINAL); 111 | } 112 | 113 | /** 114 | * 根据参数列表推断参数类型列表 115 | */ 116 | private static Class[] getParameterTypeList(Object[] args) { 117 | Class[] parameterType = new Class[args.length]; 118 | for(int i = 0; i < args.length; i++) { 119 | parameterType[i] = args[i].getClass(); 120 | } 121 | return parameterType; 122 | } 123 | 124 | /** 125 | * 调用某个方法,自动推断要调用的方法的参数类型列表 126 | */ 127 | @SuppressWarnings("UnusedReturnValue") 128 | @SneakyThrows 129 | public static Object invokeMethod(Object obj, String methodName, Object... args) { 130 | return invokeMethod(obj, methodName, getParameterTypeList(args), args); 131 | } 132 | 133 | @SneakyThrows 134 | public static Object invokeMethod(Object obj, String methodName, Class[] parameterType, Object... args) { 135 | Method m = getMethod(obj, methodName, parameterType); 136 | return m.invoke(obj, args); 137 | } 138 | 139 | @SneakyThrows 140 | public static Object invokeMethod(Class clazz, String methodName, Object... args) { 141 | return invokeMethod(clazz, methodName, getParameterTypeList(args), args); 142 | } 143 | 144 | @SneakyThrows 145 | public static Object invokeMethod(Class clazz, String methodName, Class[] parameterType, Object... args) { 146 | Method m = getMethod(clazz, methodName, parameterType); 147 | return m.invoke(null, args); 148 | } 149 | 150 | /** 151 | * 创建新实例 152 | */ 153 | @SneakyThrows 154 | public static T newInstance(Class clazz, Object... args) { 155 | Class[] parameterType = getParameterTypeList(args); 156 | Constructor constructor = clazz.getDeclaredConstructor(parameterType); 157 | constructor.setAccessible(true); 158 | return constructor.newInstance(args); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /honoka-utils/src/main/java/de/honoka/sdk/util/various/Retrier.java: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.util.various; 2 | 3 | import de.honoka.sdk.util.basic.ThrowsRunnable; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import lombok.SneakyThrows; 7 | 8 | import java.util.Arrays; 9 | import java.util.Collections; 10 | import java.util.List; 11 | import java.util.concurrent.Callable; 12 | 13 | /** 14 | * 用于忽略指定类型的异常,多次尝试执行一段代码 15 | */ 16 | public class Retrier { 17 | 18 | /** 19 | * 默认尝试次数 20 | */ 21 | @Setter 22 | @Getter 23 | protected int defaultRetryingTimes = 3; 24 | 25 | /** 26 | * 要忽略的异常类型集合 27 | */ 28 | private final List> ignoredThrowableTypes; 29 | 30 | public Retrier(List> types, int defaultRetryingTimes) { 31 | this(types); 32 | this.defaultRetryingTimes = defaultRetryingTimes; 33 | } 34 | 35 | @SafeVarargs 36 | public Retrier(Class... types) { 37 | this(Arrays.asList(types)); 38 | } 39 | 40 | public Retrier(List> types) { 41 | ignoredThrowableTypes = types; 42 | } 43 | 44 | //默认忽略Throwable 45 | public Retrier() { 46 | this(Collections.singletonList(Throwable.class)); 47 | } 48 | 49 | /** 50 | * 指定尝试次数,多次尝试执行一段代码,返回指定类型的返回值 51 | */ 52 | @SneakyThrows 53 | public T tryCode(int times, Callable callable) { 54 | outerLoop: 55 | //i表示第几次尝试 56 | for(int i = 1; ; i++) { 57 | try { 58 | return callable.call(); 59 | } catch(Throwable t) { 60 | //达到最大尝试次数,无条件抛出 61 | if(i >= times) throw t; 62 | //判断是否是要忽略的异常类型,如果是则忽略并进行下一次尝试,不是则抛出 63 | for(Class type : ignoredThrowableTypes) { 64 | //捕获类型是否是要忽略类型的子类 65 | if(type.isAssignableFrom(t.getClass())) continue outerLoop; 66 | } 67 | throw t; 68 | } 69 | } 70 | } 71 | 72 | /** 73 | * 使用默认尝试次数,多次尝试执行一段代码,返回指定类型的返回值 74 | */ 75 | public T tryCode(Callable callable) { 76 | return tryCode(defaultRetryingTimes, callable); 77 | } 78 | 79 | /** 80 | * 指定尝试次数,多次尝试执行一段代码,不返回值 81 | */ 82 | public void tryCode(int times, ThrowsRunnable runnable) { 83 | tryCode(times, () -> { 84 | runnable.run(); 85 | return null; 86 | }); 87 | } 88 | 89 | /** 90 | * 使用默认尝试次数,多次尝试执行一段代码,不返回值 91 | */ 92 | public void tryCode(ThrowsRunnable runnable) { 93 | tryCode(defaultRetryingTimes, runnable); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /honoka-utils/src/main/java/de/honoka/sdk/util/various/RuntimeUtils.java: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.util.various; 2 | 3 | import cn.hutool.core.util.CharsetUtil; 4 | import cn.hutool.core.util.RuntimeUtil; 5 | 6 | import java.nio.charset.Charset; 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | 10 | public class RuntimeUtils { 11 | 12 | public static class Commands { 13 | 14 | private final Map commands = new HashMap<>(); 15 | 16 | private Charset charset = CharsetUtil.systemCharset(); 17 | 18 | public Commands charset(Charset charset) { 19 | this.charset = charset; 20 | return this; 21 | } 22 | 23 | @SuppressWarnings("UnusedReturnValue") 24 | public Commands set(SystemEnum system, String... cmdParts) { 25 | commands.put(system, cmdParts); 26 | return this; 27 | } 28 | 29 | public Commands win(String... cmdParts) { 30 | set(SystemEnum.WINDOWS, cmdParts); 31 | return this; 32 | } 33 | 34 | public Commands linux(String... cmdParts) { 35 | set(SystemEnum.LINUX, cmdParts); 36 | return this; 37 | } 38 | 39 | public Commands mac(String... cmdParts) { 40 | set(SystemEnum.MACOS, cmdParts); 41 | return this; 42 | } 43 | 44 | public Commands other(String... cmdParts) { 45 | set(SystemEnum.OTHER, cmdParts); 46 | return this; 47 | } 48 | } 49 | 50 | public static String exec(Commands commands) { 51 | SystemEnum system = SystemEnum.getLocal(); 52 | String[] cmdParts = commands.commands.get(system); 53 | if(cmdParts == null) { 54 | throw new RuntimeException("No suitable command."); 55 | } 56 | return RuntimeUtil.execForStr(commands.charset, commands.commands.get(system)); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /honoka-utils/src/main/java/de/honoka/sdk/util/various/SystemEnum.java: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.util.various; 2 | 3 | import java.util.Locale; 4 | 5 | public enum SystemEnum { 6 | 7 | WINDOWS, LINUX, MACOS, OTHER; 8 | 9 | public static SystemEnum getLocal() { 10 | String osName = System.getProperty("os.name").toLowerCase(Locale.ROOT); 11 | if(osName.contains("windows")) return WINDOWS; 12 | if(osName.contains("linux")) return LINUX; 13 | if(osName.contains("mac")) return MACOS; 14 | return OTHER; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /honoka-utils/src/main/java/de/honoka/sdk/util/various/SystemInfoBean.java: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.util.various; 2 | 3 | import lombok.Getter; 4 | 5 | import java.lang.management.ManagementFactory; 6 | import java.lang.management.MemoryMXBean; 7 | import java.lang.management.MemoryUsage; 8 | import java.text.NumberFormat; 9 | import java.util.regex.Pattern; 10 | 11 | /** 12 | * 用于获取内存使用信息 13 | */ 14 | @Getter 15 | public strictfp class SystemInfoBean { 16 | 17 | /** 18 | * 堆、非堆内存用量 19 | */ 20 | private final MemoryUsage heap; 21 | 22 | private final MemoryUsage nonHeap; 23 | 24 | public static String byteNumToStr(long byteNum) { 25 | if(byteNum < 1024) 26 | return byteNum + "B"; 27 | if(byteNum < 1024 * 1024) 28 | return getDecimalStr(byteNum / 1024.0, 2) + "KB"; 29 | if(byteNum < 1024 * 1024 * 1024) 30 | return getDecimalStr(byteNum / 1024.0 / 1024.0, 2) + "MB"; 31 | return getDecimalStr(byteNum / 1024.0 / 1024.0 / 1024.0, 32 | 2) + "GB"; 33 | } 34 | 35 | public static String getDecimalStr(double decimal, int digit) { 36 | NumberFormat numberFormat = NumberFormat.getInstance(); 37 | numberFormat.setMaximumFractionDigits(20); 38 | numberFormat.setGroupingUsed(false); 39 | //小数转字符串 40 | String str = numberFormat.format(decimal); 41 | int pointIndex = str.indexOf("."); 42 | //没有小数点 43 | if(pointIndex == -1) return str; 44 | //有小数点,截取指定位数小数 45 | int endIndex = pointIndex + digit + 1; 46 | if(endIndex < str.length()) { 47 | str = str.substring(0, endIndex); 48 | } 49 | //判断小数部分是否全0,是则只取整数部分 50 | String[] parts = str.split("\\."); 51 | if(Pattern.matches("0*", parts[1])) { 52 | return parts[0]; 53 | } 54 | //判断最后一位不是0的数 55 | for(int i = str.length() - 1; i > pointIndex; i--) { 56 | char c = str.charAt(i); 57 | if(c != '0') return str.substring(0, i + 1); 58 | } 59 | throw new NumberFormatException(String.valueOf(decimal)); 60 | } 61 | 62 | public SystemInfoBean() { 63 | MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); 64 | heap = memoryMXBean.getHeapMemoryUsage(); 65 | nonHeap = memoryMXBean.getNonHeapMemoryUsage(); 66 | } 67 | 68 | private String getInit(MemoryUsage mu) { 69 | return byteNumToStr(mu.getInit()); 70 | } 71 | 72 | private String getUsed(MemoryUsage mu) { 73 | return byteNumToStr(mu.getUsed()); 74 | } 75 | 76 | private String getPercentUsage(MemoryUsage mu) { 77 | String usageStr = getDecimalStr((mu.getUsed() / 78 | (double) mu.getCommitted()) * 100, 2); 79 | return usageStr + "%"; 80 | } 81 | 82 | private String getMax(MemoryUsage mu) { 83 | return byteNumToStr(mu.getMax()); 84 | } 85 | 86 | private String getCommited(MemoryUsage mu) { 87 | return byteNumToStr(mu.getCommitted()); 88 | } 89 | 90 | //Heap 91 | 92 | public String getHeapInit() { 93 | return getInit(heap); 94 | } 95 | 96 | public String getHeapUsed() { 97 | return getUsed(heap); 98 | } 99 | 100 | public String getHeapPercentUsage() { 101 | return getPercentUsage(heap); 102 | } 103 | 104 | public String getHeapMax() { 105 | return getMax(heap); 106 | } 107 | 108 | public String getHeapCommited() { 109 | return getCommited(heap); 110 | } 111 | 112 | //Non Heap 113 | 114 | public String getNonHeapInit() { 115 | return getInit(nonHeap); 116 | } 117 | 118 | public String getNonHeapUsed() { 119 | return getUsed(nonHeap); 120 | } 121 | 122 | public String getNonHeapPercentUsage() { 123 | return getPercentUsage(nonHeap); 124 | } 125 | 126 | public String getNonHeapMax() { 127 | return getMax(nonHeap); 128 | } 129 | 130 | public String getNonHeapCommited() { 131 | return getCommited(nonHeap); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /honoka-utils/src/main/java/de/honoka/sdk/util/web/ApiResponse.java: -------------------------------------------------------------------------------- 1 | package de.honoka.sdk.util.web; 2 | 3 | import cn.hutool.http.HttpStatus; 4 | import cn.hutool.json.JSONUtil; 5 | import lombok.Data; 6 | import lombok.experimental.Accessors; 7 | 8 | @Data 9 | @Accessors(chain = true) 10 | public class ApiResponse { 11 | 12 | private Integer code; 13 | 14 | private Boolean status; 15 | 16 | private String msg; 17 | 18 | private T data; 19 | 20 | private ApiResponse() {} 21 | 22 | public static ApiResponse of() { 23 | return new ApiResponse<>(); 24 | } 25 | 26 | public static ApiResponse success(String msg, T1 data) { 27 | return new ApiResponse() 28 | .setCode(HttpStatus.HTTP_OK) 29 | .setStatus(true) 30 | .setMsg(msg) 31 | .setData(data); 32 | } 33 | 34 | public static ApiResponse success(T1 data) { 35 | return success(null, data); 36 | } 37 | 38 | public static ApiResponse success() { 39 | return success(null); 40 | } 41 | 42 | public static ApiResponse fail(int httpStatus, String msg) { 43 | return new ApiResponse<>() 44 | .setCode(httpStatus) 45 | .setStatus(false) 46 | .setMsg(msg) 47 | .setData(null); 48 | } 49 | 50 | public static ApiResponse fail(String msg) { 51 | return fail(HttpStatus.HTTP_INTERNAL_ERROR, msg); 52 | } 53 | 54 | public String toJsonString() { 55 | return JSONUtil.toJsonStr(this); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /honoka-utils/src/main/resources/img/java.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kosaka-bun/honoka-sdk/7bc41d75805cae4f98d5e141b84d37f1dd22dd32/honoka-utils/src/main/resources/img/java.png -------------------------------------------------------------------------------- /honoka-utils/src/main/resources/text.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 文本显示 6 | 17 | 18 | 19 |

%s
20 | 21 | 22 | -------------------------------------------------------------------------------- /scripts/github/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | cd $(dirname "$0")/../.. 6 | PROJECT_PATH="$(pwd)" 7 | 8 | chmod +x gradlew 9 | 10 | # 读取当前gradle项目根模块的版本信息,检查版本号是否符合要求 11 | ./gradlew checkVersionOfProjects 12 | check_version_of_projects_out=$(./gradlew checkVersionOfProjects) 13 | 14 | # 15 | # 检查版本号 16 | # 17 | # 当grep命令未找到匹配的字符串时,将返回非0的返回值(返回值为Exit Code,不是程序的输出内容, 18 | # 可通过“$?”得到上一行命令的返回值)。 19 | # 文件设置了set -e,任何一行命令返回值不为0时,均会中止脚本的执行,在命令后加上“|| true”可 20 | # 忽略单行命令的异常。 21 | # true是一个shell命令,它的返回值始终为0,false命令的返回值始终为1。 22 | # 23 | projects_passed=$(echo "$check_version_of_projects_out" | grep -i "results.projectsPassed=true") || true 24 | dependencies_passed=$(echo "$check_version_of_projects_out" | grep -i "results.dependenciesPassed=true") || true 25 | # -z表示字符串为空,-n表示字符串不为空 26 | if [ -n $projects_passed ] && [ -z $dependencies_passed ]; then 27 | echo 'Projects with release version contain dependencies with development version!' 28 | exit 10 29 | fi 30 | 31 | # 将kosaka-bun/maven-repo的git仓库clone到项目根目录下 32 | git clone $1 33 | 34 | # 打包,并发布到远程maven仓库在本地的一个拷贝当中 35 | repository_name=development 36 | is_development_version=true 37 | 38 | if [ -z $projects_passed ]; then 39 | echo -e '\n\nUsing development repository to publish artifacts.\n' 40 | else 41 | repository_name=release 42 | is_development_version=false 43 | fi 44 | 45 | echo "IS_DEVELOPMENT_VERSION=$is_development_version" >> "$GITHUB_OUTPUT" 46 | 47 | gradle-publish() { 48 | task_name=publish 49 | if [ -n "$1" ]; then 50 | task_name=":$1:$task_name" 51 | fi 52 | ./gradlew -PremoteMavenRepositoryUrl=$PROJECT_PATH/maven-repo/repository/$repository_name \ 53 | -PisDevelopmentRepository=$is_development_version $task_name 54 | } 55 | 56 | # 57 | # 若仅在gradle中指定task的依赖关系,无法保证在B模块依赖A模块时,在A模块的publish任务执行完成之后, 58 | # 就能在同一次构建的后续任务当中,使B模块能够从本地仓库中找到其所依赖的A模块。 59 | # 60 | # 需要根据模块间依赖关系,按顺序多次执行不同的构建。 61 | # 62 | gradle-publish honoka-utils 63 | gradle-publish honoka-kotlin-utils 64 | gradle-publish 65 | 66 | # 将maven-repo/repository目录打包,然后将tar移动到另一个单独的目录中 67 | tar -zcf maven-repo.tar.gz maven-repo/repository 68 | mkdir remote-maven-repo-copy 69 | mv maven-repo.tar.gz remote-maven-repo-copy/ -------------------------------------------------------------------------------- /scripts/github/publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | cd $(dirname "$0")/../.. 6 | PROJECT_PATH="$(pwd)" 7 | 8 | # 将kosaka-bun/maven-repo的git仓库clone到项目根目录下 9 | git clone $1 10 | 11 | # 解压maven-repo.tar.gz 12 | cd remote-maven-repo-copy 13 | tar -zxf maven-repo.tar.gz 14 | cd .. 15 | 16 | # 17 | # 将[项目根目录]/remote-maven-repo-copy/maven-repo/repository下所有内容,复制到[项目根目录]/maven-repo/repository下, 18 | # 并替换已存在的内容。 19 | # 20 | cp -rf remote-maven-repo-copy/maven-repo/repository/* maven-repo/repository/ 21 | 22 | # 进入名为maven-repo的git仓库,设置提交者信息,然后提交并推送 23 | cd maven-repo/repository 24 | 25 | commit_message='Update honoka-sdk' 26 | if [ "$IS_DEVELOPMENT_VERSION" == 'true' ]; then 27 | commit_message="$commit_message (dev)" 28 | fi 29 | 30 | git config --global user.name 'Kosaka Bun' 31 | git config --global user.email 'kosaka-bun@qq.com' 32 | git add . 33 | git commit -m "$commit_message" 34 | git push -------------------------------------------------------------------------------- /scripts/local/local-publish.bat: -------------------------------------------------------------------------------- 1 | chcp 65001 2 | 3 | cd /d "%~dp0." 4 | bash local-publish.sh 5 | -------------------------------------------------------------------------------- /scripts/local/local-publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | cd $(dirname "$0")/../.. 6 | PROJECT_PATH="$(pwd)" 7 | 8 | # 发布 9 | gradle-publish() { 10 | task_name=publish 11 | if [ -n "$1" ]; then 12 | task_name=":$1:$task_name" 13 | fi 14 | ./gradlew $task_name 15 | } 16 | 17 | gradle-publish honoka-utils 18 | gradle-publish honoka-kotlin-utils 19 | gradle-publish 20 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage") 2 | 3 | dependencyResolutionManagement { 4 | repositories { 5 | mavenLocal() 6 | maven("https://maven.aliyun.com/repository/public") 7 | mavenCentral() 8 | maven("https://mirrors.honoka.de/maven-repo/release") 9 | maven("https://mirrors.honoka.de/maven-repo/development") 10 | } 11 | } 12 | 13 | pluginManagement { 14 | repositories { 15 | maven("https://maven.aliyun.com/repository/gradle-plugin") 16 | mavenCentral() 17 | gradlePluginPortal() 18 | } 19 | } 20 | 21 | rootProject.name = "honoka-sdk" 22 | 23 | include("honoka-utils") 24 | include("honoka-kotlin-utils") 25 | include("honoka-spring-boot-starter") 26 | --------------------------------------------------------------------------------