commit a9e0e16c293ab8e37db92d77cbc7512374bd52f9 Author: 宅房 Date: Fri Nov 28 16:23:32 2025 +0800 fix bugs diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..8cfd370 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,21 @@ +# http://editorconfig.org +root = true + +# 空格替代Tab缩进在各种编辑工具下效果一致 +[*] +indent_style = space +indent_size = 4 +charset = utf-8 +end_of_line = lf +trim_trailing_whitespace = true +insert_final_newline = true + +[*.java] +indent_style = tab + +[*.{json,yml}] +indent_size = 2 + +[*.md] +insert_final_newline = false +trim_trailing_whitespace = false diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..efc7569 --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +# maven # +target + +logs + +# windows # +Thumbs.db + +# Mac # +.DS_Store + +# eclipse # +.settings +.project +.classpath +.log +*.class + +# idea # +.idea +*.iml + +# Package Files # +*.jar +*.war +*.ear +/target + +# Flattened pom +.flattened-pom.xml +/**/.flattened-pom.xml diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..aaac4ce --- /dev/null +++ b/LICENSE @@ -0,0 +1,35 @@ +BladeX商业授权许可协议 + +一、 知识产权: +BladeX系列产品知识产权归上海布雷德科技有限公司独立所有 + +二、 许可: +1. 在您完全接受并遵守本协议的基础上,本协议授予您使用BladeX的某些权利和非独占性许可。 +2. 本协议中,将本产品使用用途分为"专业版用途"和"企业版用途"。 +3. "专业版用途"定义:指个人在非团体机构中出于任何合法目的使用本产品(任何目的包括商业目的或非盈利目的)。 +4. "企业版用途"定义:指拥有合法执照的团体机构(例如公司企业、政府、学校、军队、医院、社会团体等各类组织)(不包含集团,若集团使用则需为各个子公司分别购买企业授权)出于任何合法目的使用本产品(任何目的包括商业目的或非盈利目的)。 +5. 若您不能以拥有合法执照的团体机构名义购买企业版,则视为个人名义购买,仅可行使专业版用途。在遵守此协议的前提下,后续有一次机会将企业版授权免费绑定至法人为购买人的新公司,并从专业版用途转为企业版用途。 + +三、 约束和限制: +1. 本产品只能由您为本协议许可的目的而使用,您不得透露给任何第三方; +2. 从本产品取得的任何信息、软件、产品或服务,您不得对其进行修改、改编或基于以上内容创建同种类别的衍生产品并售卖。 +3. 您不得对本产品以及与之关联的商业授权进行发布、出租、销售、分销、抵押、转让、许可或发放子许可证。 +4. 本产品商业授权版可能包含一些独立功能或特性,这些功能只有在您购买商业授权后才可以使用。在未取得商业授权的情况下,您不得使用、尝试使用或复制这些授权版独立功能。 +5. 若您的客户要求以源码方式交付软件,需缴纳企业版授权费用,否则本产品部分不得提供源码。 + +四、 不得用于非法或禁止的用途: +您在使用本产品或服务时,不得将本产品产品或服务用于任何非法用途或本协议条款、条件和声明禁止的用途。 + +五、 免责说明: +1. 本产品按"现状"授予许可,您须自行承担使用本产品的风险。BladeX团队不对此提供任何明示、暗示或任何其它形式的担保和表示。在任何情况下,对于因使用或无法使用本软件而导致的任何损失(包括但不仅限于商业利润损失、业务中断或业务信息丢失),BladeX团队无需向您或任何第三方负责,即使BladeX团队已被告知可能会造成此类损失。在任何情况下, BladeX团队均不就任何直接的、间接的、附带的、后果性的、特别的、惩戒性的和处罚性的损害赔偿承担任何责任,无论该主张是基于保证、合同、侵权(包括疏忽)或是基于其他原因作出。 +2. 本产品可能内置有第三方服务,您应自行评估使用这些第三方服务的风险,由使用此类第三方服务而产生的纠纷,全部责任由您自行承担。 +3. BladeX团队不对使用本产品构建的网站中任何信息内容以及导致的任何版权纠纷、法律争议和后果承担任何责任,全部责任由您自行承担。 +4. BladeX团队可能会经常提供产品更新或升级,但BladeX团队没有为根据本协议许可的产品提供维护或更新的责任。 +5. BladeX团队可能会按照官方制定的答疑规则为您进行答疑,但BladeX团队没有为根据本协议许可的产品提供技术支持的义务或责任。 + +六、 权利和所有权的保留: +BladeX团队保留所有未在本协议中明确授予您的所有权利。BladeX团队保留随时更新本协议的权利,并只需公示于对应产品项目的LICENSE文件,无需征得您的事先同意且无需另行通知,更新后的内容应于公示即时生效。您可以随时访问产品地址并查阅最新版许可条款,在更新生效后您继续使用本产品则被视作您已接受了新的条款。 + +七、 协议终止 +1. 您一旦开始复制、下载、安装或者使用本产品,即被视为完全理解并接受本协议的各项条款,在享有上述条款授予的许可权力同时,也受到相关的约束和限制,本协议许可范围以外的行为,将直接违反本协议并构成侵权。 +2. 一旦您违反本协议的条款,BladeX团队随时可能终止本协议、收回许可和授权,并要求您承担相应法律和经济责任。 diff --git a/README.md b/README.md new file mode 100644 index 0000000..34ad2f0 --- /dev/null +++ b/README.md @@ -0,0 +1,43 @@ +## 版权声明 +* BladeX是一个商业化软件,系列产品知识产权归**上海布雷德科技有限公司**独立所有 +* 您一旦开始复制、下载、安装或者使用本产品,即被视为完全理解并接受本协议的各项条款 +* 更多详情请看:[BladeX商业授权许可协议](https://license.bladex.cn) + +## 答疑流程 +>1. 遇到问题或Bug +>2. 业务型问题打断点调试尝试找出问题所在 +>3. 系统型问题通过百度、谷歌、社区查找解决方案 +>4. 未解决问题则进入技术社区进行发帖提问:[https://sns.bladex.cn](https://sns.bladex.cn) +>5. 将帖子地址发至商业群,特别简单三言两语就能描述清楚的也可在答疑时间内发至商业群提问 +>6. 发帖的时候一定要描述清楚,详细描述遇到问题的**重现步骤**、**报错详细信息**、**相关代码与逻辑**、**使用软件版本**以及**操作系统版本**,否则随意发帖提问将会提高我们的答疑难度。 + +## 答疑时间 +* 工作日:9:00 ~ 17:00 提供答疑,周末、节假日休息,暂停答疑 +* 请勿**私聊提问**,以免被其他用户的消息覆盖从而无法获得答疑 +* 答疑时间外遇到问题可以将问题发帖至[技术社区](https://sns.bladex.cn),我们后续会逐个回复 + +## 授权范围 +* 专业版:只可用于**个人学习**及**个人私活**项目,不可用于公司或团队,不可泄露给任何第三方 +* 企业版:可用于**企业名下**的任何项目,企业版员工在**未购买**专业版授权前,只授权开发**所在授权企业名下**的项目,**不得将BladeX用于个人私活** +* 共同遵守:若甲方需要您提供项目源码,则需代为甲方购买BladeX企业授权,甲方购买后续的所有项目都无需再次购买授权 + +## 商用权益 +* ✔️ 遵守[商业协议](https://license.bladex.cn)的前提下,将BladeX系列产品用于授权范围内的商用项目,并上线运营 +* ✔️ 遵守[商业协议](https://license.bladex.cn)的前提下,不限制项目数,不限制服务器数 +* ✔️ 遵守[商业协议](https://license.bladex.cn)的前提下,将自行编写的业务代码申请软件著作权 + +## 何为侵权 +* ❌ 不遵守商业协议,私自销售商业源码 +* ❌ 以任何理由将BladeX源码用于申请软件著作权 +* ❌ 将商业源码以任何途径任何理由泄露给未授权的单位或个人 +* ❌ 开发完毕项目,没有为甲方购买企业授权,向甲方提供了BladeX代码 +* ❌ 基于BladeX拓展研发与BladeX有竞争关系的衍生框架,并将其开源或销售 + +## 侵权后果 +* 情节较轻:第一次发现警告处理 +* 情节较重:封禁账号,踢出商业群,并保留追究法律责任的权利 +* 情节严重:与本地律师事务所合作,以公司名义起诉侵犯计算机软件著作权 + +## 举报有奖 +* 向官方提供有用线索并成功捣毁盗版个人或窝点,将会看成果给予 500~10000 不等的现金奖励 +* 官方唯一指定QQ:1272154962 \ No newline at end of file diff --git a/blade-bom/pom.xml b/blade-bom/pom.xml new file mode 100644 index 0000000..a69fe8c --- /dev/null +++ b/blade-bom/pom.xml @@ -0,0 +1,38 @@ + + + 4.0.0 + + BladeX-Tool + org.springblade + ${revision} + + + org.springblade.platform + blade-bom + pom + bladex统一版本配置 + + + + + org.codehaus.mojo + flatten-maven-plugin + ${maven.flatten.version} + + true + oss + + expand + remove + remove + remove + remove + + + + + + + diff --git a/blade-core-auto/README.md b/blade-core-auto/README.md new file mode 100644 index 0000000..0fe8cae --- /dev/null +++ b/blade-core-auto/README.md @@ -0,0 +1,12 @@ +# auto 代码自动生成 + +## 规划 +1. 生成 java spi +2. 用来生成 `spring.factories` +3. 考虑生成 `spring-devtools.properties` +4. 考虑?生成 `swagger` 注解 + +## 参考 +Google Auto: https://github.com/google/auto + +Spring 5 - spring-context-indexer:https://github.com/spring-projects/spring-framework/tree/master/spring-context-indexer \ No newline at end of file diff --git a/blade-core-auto/pom.xml b/blade-core-auto/pom.xml new file mode 100644 index 0000000..042c7f0 --- /dev/null +++ b/blade-core-auto/pom.xml @@ -0,0 +1,36 @@ + + + 4.0.0 + + BladeX-Tool + org.springblade + ${revision} + + + blade-core-auto + ${project.artifactId} + ${project.parent.version} + jar + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven.plugin.version} + + + + org.hibernate.validator + hibernate-validator-annotation-processor + 6.0.13.Final + + + + + + + + diff --git a/blade-core-auto/src/main/java/org/springblade/core/auto/annotation/AutoContextInitializer.java b/blade-core-auto/src/main/java/org/springblade/core/auto/annotation/AutoContextInitializer.java new file mode 100644 index 0000000..301cb99 --- /dev/null +++ b/blade-core-auto/src/main/java/org/springblade/core/auto/annotation/AutoContextInitializer.java @@ -0,0 +1,44 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.auto.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +/** + * ApplicationContextInitializer 处理 + * + * @author L.cm + */ +@Documented +@Retention(SOURCE) +@Target(TYPE) +public @interface AutoContextInitializer { +} diff --git a/blade-core-auto/src/main/java/org/springblade/core/auto/annotation/AutoEnvPostProcessor.java b/blade-core-auto/src/main/java/org/springblade/core/auto/annotation/AutoEnvPostProcessor.java new file mode 100644 index 0000000..3033a34 --- /dev/null +++ b/blade-core-auto/src/main/java/org/springblade/core/auto/annotation/AutoEnvPostProcessor.java @@ -0,0 +1,44 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.auto.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +/** + * EnvironmentPostProcessor 处理 + * + * @author L.cm + */ +@Documented +@Retention(SOURCE) +@Target(TYPE) +public @interface AutoEnvPostProcessor { +} diff --git a/blade-core-auto/src/main/java/org/springblade/core/auto/annotation/AutoFailureAnalyzer.java b/blade-core-auto/src/main/java/org/springblade/core/auto/annotation/AutoFailureAnalyzer.java new file mode 100644 index 0000000..6dc2e8b --- /dev/null +++ b/blade-core-auto/src/main/java/org/springblade/core/auto/annotation/AutoFailureAnalyzer.java @@ -0,0 +1,44 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.auto.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +/** + * FailureAnalyzer 处理 + * + * @author L.cm + */ +@Documented +@Retention(SOURCE) +@Target(TYPE) +public @interface AutoFailureAnalyzer { +} diff --git a/blade-core-auto/src/main/java/org/springblade/core/auto/annotation/AutoIgnore.java b/blade-core-auto/src/main/java/org/springblade/core/auto/annotation/AutoIgnore.java new file mode 100644 index 0000000..329fb94 --- /dev/null +++ b/blade-core-auto/src/main/java/org/springblade/core/auto/annotation/AutoIgnore.java @@ -0,0 +1,44 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.auto.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +/** + * AutoIgnore 处理 + * + * @author L.cm + */ +@Documented +@Retention(SOURCE) +@Target(TYPE) +public @interface AutoIgnore { +} diff --git a/blade-core-auto/src/main/java/org/springblade/core/auto/annotation/AutoListener.java b/blade-core-auto/src/main/java/org/springblade/core/auto/annotation/AutoListener.java new file mode 100644 index 0000000..eab5d1f --- /dev/null +++ b/blade-core-auto/src/main/java/org/springblade/core/auto/annotation/AutoListener.java @@ -0,0 +1,44 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.auto.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +/** + * ApplicationListener 处理 + * + * @author L.cm + */ +@Documented +@Retention(SOURCE) +@Target(TYPE) +public @interface AutoListener { +} diff --git a/blade-core-auto/src/main/java/org/springblade/core/auto/annotation/AutoRunListener.java b/blade-core-auto/src/main/java/org/springblade/core/auto/annotation/AutoRunListener.java new file mode 100644 index 0000000..d95be2b --- /dev/null +++ b/blade-core-auto/src/main/java/org/springblade/core/auto/annotation/AutoRunListener.java @@ -0,0 +1,44 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.auto.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +/** + * SpringApplicationRunListener 处理 + * + * @author L.cm + */ +@Documented +@Retention(SOURCE) +@Target(TYPE) +public @interface AutoRunListener { +} diff --git a/blade-core-auto/src/main/java/org/springblade/core/auto/common/AbstractBladeProcessor.java b/blade-core-auto/src/main/java/org/springblade/core/auto/common/AbstractBladeProcessor.java new file mode 100644 index 0000000..b458d21 --- /dev/null +++ b/blade-core-auto/src/main/java/org/springblade/core/auto/common/AbstractBladeProcessor.java @@ -0,0 +1,90 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.auto.common; + +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.RoundEnvironment; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.TypeElement; +import javax.tools.Diagnostic.Kind; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.Set; + +/** + * 抽象 处理器 + * + * @author L.cm + */ +public abstract class AbstractBladeProcessor extends AbstractProcessor { + + @Override + public SourceVersion getSupportedSourceVersion() { + return SourceVersion.latestSupported(); + } + + /** + * AutoService 注解处理器 + * @param annotations 注解 getSupportedAnnotationTypes + * @param roundEnv 扫描到的 注解新 + * @return 是否完成 + */ + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + try { + return processImpl(annotations, roundEnv); + } catch (Exception e) { + fatalError(e); + return false; + } + } + + protected abstract boolean processImpl(Set annotations, RoundEnvironment roundEnv); + + protected void log(String msg) { + if (processingEnv.getOptions().containsKey("debug")) { + processingEnv.getMessager().printMessage(Kind.NOTE, msg); + } + } + + protected void error(String msg, Element element, AnnotationMirror annotation) { + processingEnv.getMessager().printMessage(Kind.ERROR, msg, element, annotation); + } + + protected void fatalError(Exception e) { + // We don't allow exceptions of any kind to propagate to the compiler + StringWriter writer = new StringWriter(); + e.printStackTrace(new PrintWriter(writer)); + fatalError(writer.toString()); + } + + protected void fatalError(String msg) { + processingEnv.getMessager().printMessage(Kind.ERROR, "FATAL ERROR: " + msg); + } + +} diff --git a/blade-core-auto/src/main/java/org/springblade/core/auto/common/BootAutoType.java b/blade-core-auto/src/main/java/org/springblade/core/auto/common/BootAutoType.java new file mode 100644 index 0000000..d8b3d4f --- /dev/null +++ b/blade-core-auto/src/main/java/org/springblade/core/auto/common/BootAutoType.java @@ -0,0 +1,78 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.auto.common; + +import org.springblade.core.auto.annotation.*; + +/** + * 注解类型 + * + * @author L.cm + */ +public enum BootAutoType { + /** + * Component,组合注解,添加到 spring.factories + */ + COMPONENT(BootAutoType.COMPONENT_ANNOTATION, "org.springframework.boot.autoconfigure.EnableAutoConfiguration"), + /** + * ApplicationContextInitializer 添加到 spring.factories + */ + CONTEXT_INITIALIZER(AutoContextInitializer.class.getName(), "org.springframework.context.ApplicationContextInitializer"), + /** + * ApplicationListener 添加到 spring.factories + */ + LISTENER(AutoListener.class.getName(), "org.springframework.context.ApplicationListener"), + /** + * SpringApplicationRunListener 添加到 spring.factories + */ + RUN_LISTENER(AutoRunListener.class.getName(), "org.springframework.boot.SpringApplicationRunListener"), + /** + * FailureAnalyzer 添加到 spring.factories + */ + FAILURE_ANALYZER(AutoFailureAnalyzer.class.getName(), "org.springframework.boot.diagnostics.FailureAnalyzer"), + /** + * EnvironmentPostProcessor 添加到 spring.factories + */ + ENV_POST_PROCESSOR(AutoEnvPostProcessor.class.getName(), "org.springframework.boot.env.EnvironmentPostProcessor"); + + private final String annotationName; + private final String configureKey; + + BootAutoType(String annotationName, String configureKey) { + this.annotationName = annotationName; + this.configureKey = configureKey; + } + + public final String getAnnotationName() { + return annotationName; + } + + public final String getConfigureKey() { + return configureKey; + } + + public static final String COMPONENT_ANNOTATION = "org.springframework.stereotype.Component"; +} diff --git a/blade-core-auto/src/main/java/org/springblade/core/auto/common/MultiSetMap.java b/blade-core-auto/src/main/java/org/springblade/core/auto/common/MultiSetMap.java new file mode 100644 index 0000000..d1e9998 --- /dev/null +++ b/blade-core-auto/src/main/java/org/springblade/core/auto/common/MultiSetMap.java @@ -0,0 +1,158 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.auto.common; + +import java.util.*; + +/** + * MultiSetMap + * + * @author L.cm + */ +public class MultiSetMap { + private transient final Map> map; + + public MultiSetMap() { + map = new HashMap<>(); + } + + private Set createSet() { + return new HashSet<>(); + } + + /** + * put to MultiSetMap + * + * @param key 键 + * @param value 值 + * @return boolean + */ + public boolean put(K key, V value) { + Set set = map.get(key); + if (set == null) { + set = createSet(); + if (set.add(value)) { + map.put(key, set); + return true; + } else { + throw new AssertionError("New set violated the set spec"); + } + } else if (set.add(value)) { + return true; + } else { + return false; + } + } + + /** + * 是否包含某个key + * + * @param key key + * @return 结果 + */ + public boolean containsKey(K key) { + return map.containsKey(key); + } + + /** + * 是否包含 value 中的某个值 + * + * @param value value + * @return 是否包含 + */ + public boolean containsVal(V value) { + Collection> values = map.values(); + return values.stream().anyMatch(vs -> vs.contains(value)); + } + + /** + * key 集合 + * + * @return keys + */ + public Set keySet() { + return map.keySet(); + } + + /** + * put list to MultiSetMap + * + * @param key 键 + * @param set 值列表 + * @return boolean + */ + public boolean putAll(K key, Set set) { + if (set == null) { + return false; + } else { + map.put(key, set); + return true; + } + } + + /** + * put MultiSetMap to MultiSetMap + * + * @param data MultiSetMap + * @return boolean + */ + public boolean putAll(MultiSetMap data) { + if (data == null || data.isEmpty()) { + return false; + } else { + for (K k : data.keySet()) { + this.putAll(k, data.get(k)); + } + return true; + } + } + + /** + * get List by key + * + * @param key 键 + * @return List + */ + public Set get(K key) { + return map.get(key); + } + + /** + * clear MultiSetMap + */ + public void clear() { + map.clear(); + } + + /** + * isEmpty + * + * @return isEmpty + */ + public boolean isEmpty() { + return map.isEmpty(); + } +} diff --git a/blade-core-auto/src/main/java/org/springblade/core/auto/common/Sets.java b/blade-core-auto/src/main/java/org/springblade/core/auto/common/Sets.java new file mode 100644 index 0000000..9374346 --- /dev/null +++ b/blade-core-auto/src/main/java/org/springblade/core/auto/common/Sets.java @@ -0,0 +1,52 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.auto.common; + +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * 集合 工具类 + * + * @author L.cm + */ +public class Sets { + + /** + * 不可变 集合 + * + * @param es 对象 + * @param 泛型 + * @return 集合 + */ + @SafeVarargs + public static Set ofImmutableSet(E... es) { + Objects.requireNonNull(es); + return Stream.of(es).collect(Collectors.toSet()); + } +} diff --git a/blade-core-auto/src/main/java/org/springblade/core/auto/common/TypeHelper.java b/blade-core-auto/src/main/java/org/springblade/core/auto/common/TypeHelper.java new file mode 100644 index 0000000..0abe4a8 --- /dev/null +++ b/blade-core-auto/src/main/java/org/springblade/core/auto/common/TypeHelper.java @@ -0,0 +1,132 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.auto.common; + +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.QualifiedNameable; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.Types; +import java.util.ArrayList; +import java.util.List; + +/** + * Type utilities. + * + * @author Stephane Nicoll + * @since 5.0 + */ +public class TypeHelper { + + private final ProcessingEnvironment env; + + private final Types types; + + + public TypeHelper(ProcessingEnvironment env) { + this.env = env; + this.types = env.getTypeUtils(); + } + + + public String getType(Element element) { + return getType(element != null ? element.asType() : null); + } + + public String getType(AnnotationMirror annotation) { + return getType(annotation != null ? annotation.getAnnotationType() : null); + } + + public String getType(TypeMirror type) { + if (type == null) { + return null; + } + if (type instanceof DeclaredType) { + DeclaredType declaredType = (DeclaredType) type; + Element enclosingElement = declaredType.asElement().getEnclosingElement(); + if (enclosingElement != null && enclosingElement instanceof TypeElement) { + return getQualifiedName(enclosingElement) + "$" + declaredType.asElement().getSimpleName().toString(); + } else { + return getQualifiedName(declaredType.asElement()); + } + } + return type.toString(); + } + + private String getQualifiedName(Element element) { + if (element instanceof QualifiedNameable) { + return ((QualifiedNameable) element).getQualifiedName().toString(); + } + return element.toString(); + } + + /** + * Return the super class of the specified {@link Element} or null if this + * {@code element} represents {@link Object}. + * + * @param element Element + * @return Element + */ + public Element getSuperClass(Element element) { + List superTypes = this.types.directSupertypes(element.asType()); + if (superTypes.isEmpty()) { + // reached java.lang.Object + return null; + } + return this.types.asElement(superTypes.get(0)); + } + + /** + * Return the interfaces that are directly implemented by the + * specified {@link Element} or an empty list if this {@code element} does not + * implement any interface. + * + * @param element Element + * @return Element list + */ + public List getDirectInterfaces(Element element) { + List superTypes = this.types.directSupertypes(element.asType()); + List directInterfaces = new ArrayList<>(); + // index 0 is the super class + if (superTypes.size() > 1) { + for (int i = 1; i < superTypes.size(); i++) { + Element e = this.types.asElement(superTypes.get(i)); + if (e != null) { + directInterfaces.add(e); + } + } + } + return directInterfaces; + } + + public List getAllAnnotationMirrors(Element e) { + return this.env.getElementUtils().getAllAnnotationMirrors(e); + } + +} diff --git a/blade-core-auto/src/main/java/org/springblade/core/auto/factories/AutoFactoriesProcessor.java b/blade-core-auto/src/main/java/org/springblade/core/auto/factories/AutoFactoriesProcessor.java new file mode 100644 index 0000000..f8b7831 --- /dev/null +++ b/blade-core-auto/src/main/java/org/springblade/core/auto/factories/AutoFactoriesProcessor.java @@ -0,0 +1,305 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.auto.factories; + +import org.springblade.core.auto.annotation.AutoIgnore; +import org.springblade.core.auto.common.AbstractBladeProcessor; +import org.springblade.core.auto.common.BootAutoType; +import org.springblade.core.auto.common.MultiSetMap; +import org.springblade.core.auto.service.AutoService; + +import javax.annotation.processing.*; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.TypeElement; +import javax.lang.model.util.Elements; +import javax.tools.FileObject; +import javax.tools.StandardLocation; +import java.io.IOException; +import java.io.OutputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * spring boot 自动配置处理器 + * + * @author L.cm + */ +@AutoService(Processor.class) +@SupportedAnnotationTypes("*") +@SupportedOptions("debug") +public class AutoFactoriesProcessor extends AbstractBladeProcessor { + /** + * 处理的注解 @FeignClient + */ + private static final String FEIGN_CLIENT_ANNOTATION = "org.springframework.cloud.openfeign.FeignClient"; + /** + * Feign 自动配置 + */ + private static final String FEIGN_AUTO_CONFIGURE_KEY = "org.springblade.core.cloud.feign.BladeFeignAutoConfiguration"; + /** + * The location to look for factories. + *

Can be present in multiple JAR files. + */ + private static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories"; + /** + * devtools,有 Configuration 注解的 jar 一般需要 devtools 配置文件 + */ + private static final String DEVTOOLS_RESOURCE_LOCATION = "META-INF/spring-devtools.properties"; + /** + * AutoConfiguration 注解 + */ + private static final String AUTO_CONFIGURATION = "org.springframework.boot.autoconfigure.AutoConfiguration"; + /** + * AutoConfiguration imports out put + */ + private static final String AUTO_CONFIGURATION_IMPORTS_LOCATION = "META-INF/spring/" + AUTO_CONFIGURATION + ".imports"; + /** + * 数据承载 + */ + private final MultiSetMap factories = new MultiSetMap<>(); + /** + * spring boot 2.7 @AutoConfiguration + */ + private final Set autoConfigurationImportsSet = new LinkedHashSet<>(); + /** + * 元素辅助类 + */ + private Elements elementUtils; + + @Override + public synchronized void init(ProcessingEnvironment processingEnv) { + super.init(processingEnv); + elementUtils = processingEnv.getElementUtils(); + } + + @Override + protected boolean processImpl(Set annotations, RoundEnvironment roundEnv) { + if (roundEnv.processingOver()) { + // 1. 生成 spring boot 2.7.x @AutoConfiguration + generateAutoConfigurationImportsFiles(); + // 2. 生成 spring.factories + generateFactoriesFiles(); + } else { + processAnnotations(annotations, roundEnv); + } + return false; + } + + private void processAnnotations(Set annotations, RoundEnvironment roundEnv) { + // 日志 打印信息 gradle build --debug + log(annotations.toString()); + Set elementSet = roundEnv.getRootElements(); + log("All Element set: " + elementSet.toString()); + + // 过滤 TypeElement + Set typeElementSet = elementSet.stream() + .filter(this::isClassOrInterface) + .filter(TypeElement.class::isInstance) + .map(e -> (TypeElement) e) + .collect(Collectors.toSet()); + // 如果为空直接跳出 + if (typeElementSet.isEmpty()) { + log("Annotations elementSet is isEmpty"); + return; + } + + for (TypeElement typeElement : typeElementSet) { + if (isAnnotation(elementUtils, typeElement, AutoIgnore.class.getName())) { + log("Found @AutoIgnore annotation,ignore Element: " + typeElement.toString()); + } else if (isAnnotation(elementUtils, typeElement, FEIGN_CLIENT_ANNOTATION)) { + log("Found @FeignClient Element: " + typeElement.toString()); + + ElementKind elementKind = typeElement.getKind(); + // Feign Client 只处理 接口 + if (ElementKind.INTERFACE != elementKind) { + fatalError("@FeignClient Element " + typeElement + " 不是接口。"); + continue; + } + + String factoryName = typeElement.getQualifiedName().toString(); + if (factories.containsVal(factoryName)) { + continue; + } + + log("读取到新配置 spring.factories factoryName:" + factoryName); + factories.put(FEIGN_AUTO_CONFIGURE_KEY, factoryName); + } else { + // 1. 生成 2.7.x 的 spi + if (isAnnotation(elementUtils, typeElement, BootAutoType.COMPONENT_ANNOTATION)) { + String autoConfigurationBeanName = typeElement.getQualifiedName().toString(); + autoConfigurationImportsSet.add(autoConfigurationBeanName); + log("读取到自动配置 @AutoConfiguration:" + autoConfigurationBeanName); + } + // 2. 老的 spring.factories + for (BootAutoType autoType : BootAutoType.values()) { + String annotation = autoType.getAnnotationName(); + if (isAnnotation(elementUtils, typeElement, annotation)) { + log("Found @" + annotation + " Element: " + typeElement.toString()); + + String factoryName = typeElement.getQualifiedName().toString(); + if (factories.containsVal(factoryName)) { + continue; + } + + log("读取到新配置 spring.factories factoryName:" + factoryName); + factories.put(autoType.getConfigureKey(), factoryName); + } + } + } + } + } + + private void generateFactoriesFiles() { + if (factories.isEmpty()) { + return; + } + Filer filer = processingEnv.getFiler(); + try { + // spring.factories 配置 + MultiSetMap allFactories = new MultiSetMap<>(); + // 1. 用户手动配置项目下的 spring.factories 文件 + try { + FileObject existingFactoriesFile = filer.getResource(StandardLocation.SOURCE_OUTPUT, "", FACTORIES_RESOURCE_LOCATION); + // 查找是否已经存在 spring.factories + log("Looking for existing spring.factories file at " + existingFactoriesFile.toUri()); + MultiSetMap existingFactories = FactoriesFiles.readFactoriesFile(existingFactoriesFile, elementUtils); + log("Existing spring.factories entries: " + existingFactories); + allFactories.putAll(existingFactories); + } catch (IOException e) { + log("spring.factories resource file not found."); + } + // 2. 增量编译,已经存在的 spring.factories 文件 + try { + FileObject existingFactoriesFile = filer.getResource(StandardLocation.CLASS_OUTPUT, "", FACTORIES_RESOURCE_LOCATION); + // 查找是否已经存在 spring.factories + log("Looking for existing spring.factories file at " + existingFactoriesFile.toUri()); + MultiSetMap existingFactories = FactoriesFiles.readFactoriesFile(existingFactoriesFile, elementUtils); + log("Existing spring.factories entries: " + existingFactories); + allFactories.putAll(existingFactories); + } catch (IOException e) { + log("spring.factories resource file did not already exist."); + } + // 3. 处理器扫描出来的新的配置 + allFactories.putAll(factories); + log("New spring.factories file contents: " + allFactories); + FileObject factoriesFile = filer.createResource(StandardLocation.CLASS_OUTPUT, "", FACTORIES_RESOURCE_LOCATION); + try (OutputStream out = factoriesFile.openOutputStream()) { + FactoriesFiles.writeFactoriesFile(allFactories, out); + } + // 4. devtools 配置,因为有 @Configuration 注解的需要 devtools + String classesPath = factoriesFile.toUri().toString().split("classes")[0]; + Path projectPath = Paths.get(new URI(classesPath)).getParent(); + String projectName = projectPath.getFileName().toString(); + FileObject devToolsFile = filer.createResource(StandardLocation.CLASS_OUTPUT, "", DEVTOOLS_RESOURCE_LOCATION); + try (OutputStream out = devToolsFile.openOutputStream()) { + FactoriesFiles.writeDevToolsFile(projectName, out); + } + } catch (IOException | URISyntaxException e) { + fatalError(e); + } + } + + private void generateAutoConfigurationImportsFiles() { + if (autoConfigurationImportsSet.isEmpty()) { + return; + } + Filer filer = processingEnv.getFiler(); + try { + // AutoConfiguration 配置 + Set allAutoConfigurationImports = new LinkedHashSet<>(); + // 1. 用户手动配置项目下的 AutoConfiguration 文件 + try { + FileObject existingFactoriesFile = filer.getResource(StandardLocation.SOURCE_OUTPUT, "", AUTO_CONFIGURATION_IMPORTS_LOCATION); + // 查找是否已经存在 spring.factories + log("Looking for existing AutoConfiguration imports file at " + existingFactoriesFile.toUri()); + Set existingSet = FactoriesFiles.readAutoConfigurationImports(existingFactoriesFile); + log("Existing AutoConfiguration imports entries: " + existingSet); + allAutoConfigurationImports.addAll(existingSet); + } catch (IOException e) { + log("AutoConfiguration imports resource file not found."); + } + // 2. 增量编译,已经存在的配置文件 + try { + FileObject existingFactoriesFile = filer.getResource(StandardLocation.CLASS_OUTPUT, "", AUTO_CONFIGURATION_IMPORTS_LOCATION); + // 查找是否已经存在 spring.factories + log("Looking for existing AutoConfiguration imports file at " + existingFactoriesFile.toUri()); + Set existingSet = FactoriesFiles.readAutoConfigurationImports(existingFactoriesFile); + log("Existing AutoConfiguration imports entries: " + existingSet); + allAutoConfigurationImports.addAll(existingSet); + } catch (IOException e) { + log("AutoConfiguration imports resource file did not already exist."); + } + // 3. 处理器扫描出来的新的配置 + allAutoConfigurationImports.addAll(autoConfigurationImportsSet); + log("New AutoConfiguration imports file contents: " + allAutoConfigurationImports); + FileObject autoConfigurationImportsFile = filer.createResource(StandardLocation.CLASS_OUTPUT, "", AUTO_CONFIGURATION_IMPORTS_LOCATION); + try (OutputStream out = autoConfigurationImportsFile.openOutputStream()) { + FactoriesFiles.writeAutoConfigurationImportsFile(allAutoConfigurationImports, out); + } + } catch (IOException e) { + fatalError(e); + } + } + + private boolean isClassOrInterface(Element e) { + ElementKind kind = e.getKind(); + return kind == ElementKind.CLASS || kind == ElementKind.INTERFACE; + } + + private boolean isAnnotation(Elements elementUtils, Element e, String annotationFullName) { + List annotationList = elementUtils.getAllAnnotationMirrors(e); + for (AnnotationMirror annotation : annotationList) { + // 如果是对于的注解 + if (isAnnotation(annotationFullName, annotation)) { + return true; + } + // 处理组合注解 + Element element = annotation.getAnnotationType().asElement(); + String elementStr = element.toString(); + // 如果是 java 元注解,继续循环 + if (elementStr.startsWith("java.lang") || elementStr.startsWith("kotlin.")) { + continue; + } + // 递归处理 组合注解 + if (isAnnotation(elementUtils, element, annotationFullName)) { + return true; + } + } + return false; + } + + private boolean isAnnotation(String annotationFullName, AnnotationMirror annotation) { + return annotationFullName.equals(annotation.getAnnotationType().toString()); + } +} diff --git a/blade-core-auto/src/main/java/org/springblade/core/auto/factories/FactoriesFiles.java b/blade-core-auto/src/main/java/org/springblade/core/auto/factories/FactoriesFiles.java new file mode 100644 index 0000000..cef895a --- /dev/null +++ b/blade-core-auto/src/main/java/org/springblade/core/auto/factories/FactoriesFiles.java @@ -0,0 +1,163 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.auto.factories; + +import org.springblade.core.auto.common.MultiSetMap; + +import javax.lang.model.util.Elements; +import javax.tools.FileObject; +import java.io.*; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.stream.Collectors; + +/** + * spring boot 自动化配置工具类 + * + * @author L.cm + */ +class FactoriesFiles { + private static final Charset UTF_8 = StandardCharsets.UTF_8; + + /** + * 读取 spring.factories 文件 + * + * @param fileObject FileObject + * @return MultiSetMap + * @throws IOException 异常信息 + */ + protected static MultiSetMap readFactoriesFile(FileObject fileObject, Elements elementUtils) throws IOException { + // 读取 spring.factories 内容 + Properties properties = new Properties(); + try (InputStream input = fileObject.openInputStream()) { + properties.load(input); + } + MultiSetMap multiSetMap = new MultiSetMap<>(); + Set> entrySet = properties.entrySet(); + for (Map.Entry objectEntry : entrySet) { + String key = (String) objectEntry.getKey(); + String value = (String) objectEntry.getValue(); + if (value == null || value.trim().isEmpty()) { + continue; + } + // 解析 spring.factories + String[] values = value.split(","); + Set valueSet = Arrays.stream(values) + .filter(v -> !v.isEmpty()) + .map(String::trim) + // 校验是否删除文件 + .filter((v) -> Objects.nonNull(elementUtils.getTypeElement(v))) + .collect(Collectors.toSet()); + multiSetMap.putAll(key.trim(), valueSet); + } + return multiSetMap; + } + + /** + * 读取已经存在的 AutoConfiguration imports + * + * @param fileObject FileObject + * @return Set + * @throws IOException IOException + */ + protected static Set readAutoConfigurationImports(FileObject fileObject) throws IOException { + Set set = new HashSet<>(); + try ( + InputStream input = fileObject.openInputStream(); + BufferedReader reader = new BufferedReader(new InputStreamReader(input)) + ) { + reader.lines() + .map(String::trim) + .filter(line -> !line.startsWith("#")) + .forEach(set::add); + } + return set; + } + + /** + * 写出 spring.factories 文件 + * + * @param factories factories 信息 + * @param output 输出流 + * @throws IOException 异常信息 + */ + protected static void writeFactoriesFile(MultiSetMap factories, + OutputStream output) throws IOException { + BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(output, UTF_8)); + Set keySet = factories.keySet(); + for (String key : keySet) { + Set values = factories.get(key); + if (values == null || values.isEmpty()) { + continue; + } + writer.write(key); + writer.write("=\\\n "); + StringJoiner joiner = new StringJoiner(",\\\n "); + for (String value : values) { + joiner.add(value); + } + writer.write(joiner.toString()); + writer.newLine(); + } + writer.flush(); + output.close(); + } + + /** + * 写出 spring-devtools.properties + * + * @param projectName 项目名 + * @param output 输出流 + * @throws IOException 异常信息 + */ + protected static void writeDevToolsFile(String projectName, + OutputStream output) throws IOException { + BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(output, UTF_8)); + String format = "restart.include.%s=/%s[\\\\w-]+\\.jar"; + writer.write(String.format(format, projectName, projectName)); + writer.flush(); + output.close(); + } + + /** + * 写出 AutoConfiguration imports + * + * @param allAutoConfigurationImports allAutoConfigurationImports + * @param output OutputStream + * @throws IOException IOException + */ + protected static void writeAutoConfigurationImportsFile(Set allAutoConfigurationImports, OutputStream output) throws IOException { + BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(output, UTF_8)); + StringJoiner joiner = new StringJoiner("\n"); + for (String configurationImport : allAutoConfigurationImports) { + joiner.add(configurationImport); + } + writer.write(joiner.toString()); + writer.flush(); + } + +} diff --git a/blade-core-auto/src/main/java/org/springblade/core/auto/service/AutoService.java b/blade-core-auto/src/main/java/org/springblade/core/auto/service/AutoService.java new file mode 100644 index 0000000..d24ec85 --- /dev/null +++ b/blade-core-auto/src/main/java/org/springblade/core/auto/service/AutoService.java @@ -0,0 +1,56 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.auto.service; + +import java.lang.annotation.*; + +/** + * An annotation for service providers as described in {@link java.util.ServiceLoader}. The {@link + * AutoServiceProcessor} generates the configuration files which + * allows service providers to be loaded with {@link java.util.ServiceLoader#load(Class)}. + * + *

Service providers assert that they conform to the service provider specification. + * Specifically, they must: + * + *

+ * + * @author google + */ +@Documented +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.TYPE) +public @interface AutoService { + /** + * Returns the interfaces implemented by this service provider. + * + * @return interface array + */ + Class[] value(); +} diff --git a/blade-core-auto/src/main/java/org/springblade/core/auto/service/AutoServiceProcessor.java b/blade-core-auto/src/main/java/org/springblade/core/auto/service/AutoServiceProcessor.java new file mode 100644 index 0000000..33b3831 --- /dev/null +++ b/blade-core-auto/src/main/java/org/springblade/core/auto/service/AutoServiceProcessor.java @@ -0,0 +1,263 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.auto.service; + +import org.springblade.core.auto.common.AbstractBladeProcessor; +import org.springblade.core.auto.common.MultiSetMap; +import org.springblade.core.auto.common.Sets; +import org.springblade.core.auto.common.TypeHelper; + +import javax.annotation.processing.Filer; +import javax.annotation.processing.ProcessingEnvironment; +import javax.annotation.processing.RoundEnvironment; +import javax.annotation.processing.SupportedOptions; +import javax.lang.model.element.*; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.ElementFilter; +import javax.lang.model.util.Elements; +import javax.lang.model.util.SimpleAnnotationValueVisitor8; +import javax.lang.model.util.Types; +import javax.tools.FileObject; +import javax.tools.StandardLocation; +import java.io.IOException; +import java.io.OutputStream; +import java.lang.annotation.Annotation; +import java.util.*; +import java.util.stream.Collectors; + +/** + * java spi 服务自动处理器 参考:google auto + * + * @author L.cm + */ +@SupportedOptions("debug") +public class AutoServiceProcessor extends AbstractBladeProcessor { + /** + * spi 服务集合,key 接口 -> value 实现列表 + */ + private final MultiSetMap providers = new MultiSetMap<>(); + private TypeHelper typeHelper; + + @Override + public synchronized void init(ProcessingEnvironment env) { + super.init(env); + this.typeHelper = new TypeHelper(env); + } + + @Override + public Set getSupportedAnnotationTypes() { + return Sets.ofImmutableSet(AutoService.class.getName()); + } + + @Override + protected boolean processImpl(Set annotations, RoundEnvironment roundEnv) { + if (roundEnv.processingOver()) { + generateConfigFiles(); + } else { + processAnnotations(annotations, roundEnv); + } + return true; + } + + private void processAnnotations(Set annotations, RoundEnvironment roundEnv) { + Set elements = roundEnv.getElementsAnnotatedWith(AutoService.class); + + log(annotations.toString()); + log(elements.toString()); + + for (Element e : elements) { + TypeElement providerImplementer = (TypeElement) e; + AnnotationMirror annotationMirror = getAnnotationMirror(e, AutoService.class); + if (annotationMirror == null) { + continue; + } + Set typeMirrors = getValueFieldOfClasses(annotationMirror); + if (typeMirrors.isEmpty()) { + error("No service interfaces provided for element!", e, annotationMirror); + continue; + } + for (TypeMirror typeMirror : typeMirrors) { + String providerInterfaceName = typeHelper.getType(typeMirror); + Name providerImplementerName = providerImplementer.getQualifiedName(); + + log("provider interface: " + providerInterfaceName); + log("provider implementer: " + providerImplementerName); + + if (checkImplementer(providerImplementer, typeMirror)) { + providers.put(providerInterfaceName, typeHelper.getType(providerImplementer)); + } else { + String message = "ServiceProviders must implement their service provider interface. " + + providerImplementerName + " does not implement " + providerInterfaceName; + error(message, e, annotationMirror); + } + } + } + } + + private void generateConfigFiles() { + Filer filer = processingEnv.getFiler(); + + for (String providerInterface : providers.keySet()) { + String resourceFile = "META-INF/services/" + providerInterface; + log("Working on resource file: " + resourceFile); + try { + SortedSet allServices = new TreeSet<>(); + try { + FileObject existingFile = filer.getResource(StandardLocation.CLASS_OUTPUT, "", resourceFile); + log("Looking for existing resource file at " + existingFile.toUri()); + Set oldServices = ServicesFiles.readServiceFile(existingFile.openInputStream()); + log("Existing service entries: " + oldServices); + allServices.addAll(oldServices); + } catch (IOException e) { + log("Resource file did not already exist."); + } + + Set newServices = new HashSet<>(providers.get(providerInterface)); + if (allServices.containsAll(newServices)) { + log("No new service entries being added."); + return; + } + + allServices.addAll(newServices); + log("New service file contents: " + allServices); + FileObject fileObject = filer.createResource(StandardLocation.CLASS_OUTPUT, "", resourceFile); + OutputStream out = fileObject.openOutputStream(); + ServicesFiles.writeServiceFile(allServices, out); + out.close(); + log("Wrote to: " + fileObject.toUri()); + } catch (IOException e) { + fatalError("Unable to create " + resourceFile + ", " + e); + return; + } + } + } + + /** + * Verifies {@link java.util.spi.LocaleServiceProvider} constraints on the concrete provider class. + * Note that these constraints are enforced at runtime via the ServiceLoader, + * we're just checking them at compile time to be extra nice to our users. + */ + private boolean checkImplementer(TypeElement providerImplementer, TypeMirror providerType) { + // TODO: We're currently only enforcing the subtype relationship + // constraint. It would be nice to enforce them all. + Types types = processingEnv.getTypeUtils(); + + return types.isSubtype(providerImplementer.asType(), providerType); + } + + /** + * 读取 AutoService 上的 value 值 + * + * @param annotationMirror AnnotationMirror + * @return value 集合 + */ + private Set getValueFieldOfClasses(AnnotationMirror annotationMirror) { + return getAnnotationValue(annotationMirror, "value") + .accept(new SimpleAnnotationValueVisitor8, Void>() { + @Override + public Set visitType(TypeMirror typeMirror, Void v) { + Set declaredTypeSet = new HashSet<>(1); + declaredTypeSet.add(typeMirror); + return Collections.unmodifiableSet(declaredTypeSet); + } + + @Override + public Set visitArray( + List values, Void v) { + return values + .stream() + .flatMap(value -> value.accept(this, null).stream()) + .collect(Collectors.toSet()); + } + }, null); + } + + /** + * Returns a {@link ExecutableElement} and its associated {@link AnnotationValue} if such + * an element was either declared in the usage represented by the provided + * {@link AnnotationMirror}, or if such an element was defined with a default. + * + * @param annotationMirror AnnotationMirror + * @param elementName elementName + * @return AnnotationValue map + * @throws IllegalArgumentException if no element is defined with the given elementName. + */ + public AnnotationValue getAnnotationValue(AnnotationMirror annotationMirror, String elementName) { + Objects.requireNonNull(annotationMirror); + Objects.requireNonNull(elementName); + for (Map.Entry entry : getAnnotationValuesWithDefaults(annotationMirror).entrySet()) { + if (entry.getKey().getSimpleName().contentEquals(elementName)) { + return entry.getValue(); + } + } + String name = typeHelper.getType(annotationMirror); + throw new IllegalArgumentException(String.format("@%s does not define an element %s()", name, elementName)); + } + + /** + * Returns the {@link AnnotationMirror}'s map of {@link AnnotationValue} indexed by {@link + * ExecutableElement}, supplying default values from the annotation if the annotation property has + * not been set. This is equivalent to {@link + * Elements#getElementValuesWithDefaults(AnnotationMirror)} but can be called statically without + * an {@link Elements} instance. + * + *

The iteration order of elements of the returned map will be the order in which the {@link + * ExecutableElement}s are defined in {@code annotation}'s {@linkplain + * AnnotationMirror#getAnnotationType() type}. + * + * @param annotation AnnotationMirror + * @return AnnotationValue Map + */ + public Map getAnnotationValuesWithDefaults(AnnotationMirror annotation) { + Map values = new HashMap<>(32); + Map declaredValues = annotation.getElementValues(); + for (ExecutableElement method : ElementFilter.methodsIn(annotation.getAnnotationType().asElement().getEnclosedElements())) { + // Must iterate and put in this order, to ensure consistency in generated code. + if (declaredValues.containsKey(method)) { + values.put(method, declaredValues.get(method)); + } else if (method.getDefaultValue() != null) { + values.put(method, method.getDefaultValue()); + } else { + String name = typeHelper.getType(method); + throw new IllegalStateException( + "Unset annotation value without default should never happen: " + name + '.' + method.getSimpleName() + "()"); + } + } + return Collections.unmodifiableMap(values); + } + + public AnnotationMirror getAnnotationMirror(Element element, Class annotationClass) { + String annotationClassName = annotationClass.getCanonicalName(); + for (AnnotationMirror annotationMirror : element.getAnnotationMirrors()) { + String name = typeHelper.getType(annotationMirror); + if (name.contentEquals(annotationClassName)) { + return annotationMirror; + } + } + return null; + } + +} diff --git a/blade-core-auto/src/main/java/org/springblade/core/auto/service/ServicesFiles.java b/blade-core-auto/src/main/java/org/springblade/core/auto/service/ServicesFiles.java new file mode 100644 index 0000000..3a302f0 --- /dev/null +++ b/blade-core-auto/src/main/java/org/springblade/core/auto/service/ServicesFiles.java @@ -0,0 +1,86 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.auto.service; + +import java.io.*; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +/** + * A helper class for reading and writing Services files. + * + * @author L.cm + */ +class ServicesFiles { + private static final Charset UTF_8 = StandardCharsets.UTF_8; + + /** + * Reads the set of service classes from a service file. + * + * @param input not {@code null}. Closed after use. + * @return a not {@code null Set} of service class names. + * @throws IOException + */ + static Set readServiceFile(InputStream input) throws IOException { + HashSet serviceClasses = new HashSet<>(); + try ( + InputStreamReader isr = new InputStreamReader(input, UTF_8); + BufferedReader r = new BufferedReader(isr) + ) { + String line; + while ((line = r.readLine()) != null) { + int commentStart = line.indexOf('#'); + if (commentStart >= 0) { + line = line.substring(0, commentStart); + } + line = line.trim(); + if (!line.isEmpty()) { + serviceClasses.add(line); + } + } + return serviceClasses; + } + } + + /** + * Writes the set of service class names to a service file. + * + * @param output not {@code null}. Not closed after use. + * @param services a not {@code null Collection} of service class names. + * @throws IOException + */ + static void writeServiceFile(Collection services, OutputStream output) throws IOException { + BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(output, UTF_8)); + for (String service : services) { + writer.write(service); + writer.newLine(); + } + writer.flush(); + } +} diff --git a/blade-core-auto/src/main/resources/META-INF/services/javax.annotation.processing.Processor b/blade-core-auto/src/main/resources/META-INF/services/javax.annotation.processing.Processor new file mode 100644 index 0000000..170d3ea --- /dev/null +++ b/blade-core-auto/src/main/resources/META-INF/services/javax.annotation.processing.Processor @@ -0,0 +1,2 @@ +org.springblade.core.auto.service.AutoServiceProcessor +org.springblade.core.auto.factories.AutoFactoriesProcessor diff --git a/blade-core-boot/pom.xml b/blade-core-boot/pom.xml new file mode 100644 index 0000000..91c30b8 --- /dev/null +++ b/blade-core-boot/pom.xml @@ -0,0 +1,59 @@ + + + 4.0.0 + + BladeX-Tool + org.springblade + ${revision} + + + blade-core-boot + ${project.artifactId} + ${project.parent.version} + jar + + + + + org.springblade + blade-core-context + + + org.springblade + blade-core-db + + + org.springblade + blade-core-secure + + + org.springblade + blade-core-cloud + + + org.springblade + blade-starter-cache + + + org.springblade + blade-starter-redis + + + org.springblade + blade-starter-log + + + org.springblade + blade-starter-mybatis + + + + org.springblade + blade-core-auto + provided + + + + diff --git a/blade-core-boot/src/main/java/org/springblade/core/boot/config/BladeBootAutoConfiguration.java b/blade-core-boot/src/main/java/org/springblade/core/boot/config/BladeBootAutoConfiguration.java new file mode 100644 index 0000000..9a1590a --- /dev/null +++ b/blade-core-boot/src/main/java/org/springblade/core/boot/config/BladeBootAutoConfiguration.java @@ -0,0 +1,46 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.boot.config; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.launch.props.BladePropertySource; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.context.annotation.EnableAspectJAutoProxy; + +/** + * blade自动配置类 + * + * @author Chill + */ +@Slf4j +@AutoConfiguration +@AllArgsConstructor +@EnableAspectJAutoProxy(proxyTargetClass = true, exposeProxy = true) +@BladePropertySource(value = "classpath:/blade-boot.yml") +public class BladeBootAutoConfiguration { + +} diff --git a/blade-core-boot/src/main/java/org/springblade/core/boot/config/BladeExecutorConfiguration.java b/blade-core-boot/src/main/java/org/springblade/core/boot/config/BladeExecutorConfiguration.java new file mode 100644 index 0000000..0b49958 --- /dev/null +++ b/blade-core-boot/src/main/java/org/springblade/core/boot/config/BladeExecutorConfiguration.java @@ -0,0 +1,116 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.boot.config; + +import lombok.AllArgsConstructor; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.boot.error.ErrorType; +import org.springblade.core.boot.error.ErrorUtil; +import org.springblade.core.context.BladeContext; +import org.springblade.core.context.BladeRunnableWrapper; +import org.springblade.core.launch.props.BladeProperties; +import org.springblade.core.log.constant.EventConstant; +import org.springblade.core.log.event.ErrorLogEvent; +import org.springblade.core.log.model.LogError; +import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; +import org.springframework.boot.task.ThreadPoolTaskExecutorCustomizer; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.lang.NonNull; +import org.springframework.scheduling.annotation.AsyncConfigurerSupport; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.annotation.EnableScheduling; + +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ThreadPoolExecutor; + +/** + * 异步处理 + * + * @author Chill + */ +@Slf4j +@Configuration +@EnableAsync +@EnableScheduling +@AllArgsConstructor +public class BladeExecutorConfiguration extends AsyncConfigurerSupport { + + private final BladeContext bladeContext; + private final BladeProperties bladeProperties; + private final ApplicationEventPublisher publisher; + + @Bean + public ThreadPoolTaskExecutorCustomizer taskExecutorCustomizer() { + return taskExecutor -> { + taskExecutor.setThreadNamePrefix("async-task-"); + taskExecutor.setTaskDecorator(BladeRunnableWrapper::new); + taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + }; + } + + @Bean + public ThreadPoolTaskExecutorCustomizer taskSchedulerCustomizer() { + return taskExecutor -> { + taskExecutor.setThreadNamePrefix("async-scheduler"); + taskExecutor.setTaskDecorator(BladeRunnableWrapper::new); + taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + }; + } + + @Override + public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { + return new BladeAsyncUncaughtExceptionHandler(bladeContext, bladeProperties, publisher); + } + + @RequiredArgsConstructor + private static class BladeAsyncUncaughtExceptionHandler implements AsyncUncaughtExceptionHandler { + private final BladeContext bladeContext; + private final BladeProperties bladeProperties; + private final ApplicationEventPublisher eventPublisher; + + @Override + public void handleUncaughtException(@NonNull Throwable error, @NonNull Method method, @NonNull Object... params) { + log.error("Unexpected exception occurred invoking async method: {}", method, error); + LogError logError = new LogError(); + // 服务信息、环境、异常类型 + logError.setParams(ErrorType.ASYNC.getType()); + logError.setEnv(bladeProperties.getEnv()); + logError.setServiceId(bladeProperties.getName()); + logError.setRequestUri(bladeContext.getRequestId()); + // 堆栈信息 + ErrorUtil.initErrorInfo(error, logError); + Map event = new HashMap<>(16); + event.put(EventConstant.EVENT_LOG, logError); + eventPublisher.publishEvent(new ErrorLogEvent(event)); + } + } + +} diff --git a/blade-core-boot/src/main/java/org/springblade/core/boot/config/BladeRetryConfiguration.java b/blade-core-boot/src/main/java/org/springblade/core/boot/config/BladeRetryConfiguration.java new file mode 100644 index 0000000..757d04f --- /dev/null +++ b/blade-core-boot/src/main/java/org/springblade/core/boot/config/BladeRetryConfiguration.java @@ -0,0 +1,58 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.boot.config; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.retry.interceptor.RetryInterceptorBuilder; +import org.springframework.retry.interceptor.RetryOperationsInterceptor; + +/** + * 重试机制 + * + * @author Chill + */ +@Slf4j +@AutoConfiguration +public class BladeRetryConfiguration { + + @Bean + @ConditionalOnMissingBean(name = "configServerRetryInterceptor") + public RetryOperationsInterceptor configServerRetryInterceptor() { + log.info(String.format( + "configServerRetryInterceptor: Changing backOffOptions " + + "to initial: %s, multiplier: %s, maxInterval: %s", + 1000, 1.2, 5000)); + return RetryInterceptorBuilder + .stateless() + .backOffOptions(1000, 1.2, 5000) + .maxAttempts(10) + .build(); + } + +} diff --git a/blade-core-boot/src/main/java/org/springblade/core/boot/config/BladeWebMvcConfiguration.java b/blade-core-boot/src/main/java/org/springblade/core/boot/config/BladeWebMvcConfiguration.java new file mode 100644 index 0000000..2634f56 --- /dev/null +++ b/blade-core-boot/src/main/java/org/springblade/core/boot/config/BladeWebMvcConfiguration.java @@ -0,0 +1,71 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.boot.config; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.boot.props.BladeFileProperties; +import org.springblade.core.boot.props.BladeUploadProperties; +import org.springblade.core.boot.resolver.TokenArgumentResolver; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.List; + +/** + * WEB配置 + * + * @author Chill + */ +@Slf4j +@AutoConfiguration +@Order(Ordered.HIGHEST_PRECEDENCE) +@AllArgsConstructor +@EnableConfigurationProperties({ + BladeUploadProperties.class, BladeFileProperties.class +}) +public class BladeWebMvcConfiguration implements WebMvcConfigurer { + + private final BladeUploadProperties bladeUploadProperties; + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + String path = bladeUploadProperties.getSavePath(); + registry.addResourceHandler("/upload/**") + .addResourceLocations("file:" + path + "/upload/"); + } + + @Override + public void addArgumentResolvers(List argumentResolvers) { + argumentResolvers.add(new TokenArgumentResolver()); + } + +} diff --git a/blade-core-boot/src/main/java/org/springblade/core/boot/config/RequestConfiguration.java b/blade-core-boot/src/main/java/org/springblade/core/boot/config/RequestConfiguration.java new file mode 100644 index 0000000..0975d7c --- /dev/null +++ b/blade-core-boot/src/main/java/org/springblade/core/boot/config/RequestConfiguration.java @@ -0,0 +1,66 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.boot.config; + +import lombok.AllArgsConstructor; +import org.springblade.core.boot.request.BladeRequestFilter; +import org.springblade.core.boot.request.RequestProperties; +import org.springblade.core.boot.request.XssProperties; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.core.Ordered; + +import jakarta.servlet.DispatcherType; + +/** + * 过滤器配置类 + * + * @author Chill + */ +@AutoConfiguration +@AllArgsConstructor +@EnableConfigurationProperties({RequestProperties.class, XssProperties.class}) +public class RequestConfiguration { + + private final RequestProperties requestProperties; + private final XssProperties xssProperties; + + /** + * 全局过滤器 + */ + @Bean + public FilterRegistrationBean bladeFilterRegistration() { + FilterRegistrationBean registration = new FilterRegistrationBean<>(); + registration.setDispatcherTypes(DispatcherType.REQUEST); + registration.setFilter(new BladeRequestFilter(requestProperties, xssProperties)); + registration.addUrlPatterns("/*"); + registration.setName("bladeRequestFilter"); + registration.setOrder(Ordered.LOWEST_PRECEDENCE); + return registration; + } +} diff --git a/blade-core-boot/src/main/java/org/springblade/core/boot/ctrl/BladeController.java b/blade-core-boot/src/main/java/org/springblade/core/boot/ctrl/BladeController.java new file mode 100644 index 0000000..5efdeba --- /dev/null +++ b/blade-core-boot/src/main/java/org/springblade/core/boot/ctrl/BladeController.java @@ -0,0 +1,288 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.boot.ctrl; + +import org.springblade.core.boot.file.LocalFile; +import org.springblade.core.boot.file.BladeFileUtil; +import org.springblade.core.secure.BladeUser; +import org.springblade.core.secure.utils.AuthUtil; +import org.springblade.core.tool.api.R; +import org.springblade.core.tool.utils.Charsets; +import org.springblade.core.tool.utils.StringPool; +import org.springblade.core.tool.utils.WebUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.ResourceRegion; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.util.UriUtils; + +import jakarta.servlet.http.HttpServletRequest; +import java.io.File; +import java.io.IOException; +import java.util.List; + +/** + * Blade控制器封装类 + * + * @author Chill + */ +public class BladeController { + + /** + * ============================ REQUEST ================================================= + */ + + @Autowired + private HttpServletRequest request; + + /** + * 获取request + */ + public HttpServletRequest getRequest() { + return this.request; + } + + /** + * 获取当前用户 + * + * @return + */ + public BladeUser getUser() { + return AuthUtil.getUser(); + } + + /** ============================ API_RESULT ================================================= */ + + /** + * 返回ApiResult + * + * @param data + * @return R + */ + public R data(T data) { + return R.data(data); + } + + /** + * 返回ApiResult + * + * @param data + * @param message + * @return R + */ + public R data(T data, String message) { + return R.data(data, message); + } + + /** + * 返回ApiResult + * + * @param data + * @param message + * @param code + * @return R + */ + public R data(T data, String message, int code) { + return R.data(code, data, message); + } + + /** + * 返回ApiResult + * + * @param message + * @return R + */ + public R success(String message) { + return R.success(message); + } + + /** + * 返回ApiResult + * + * @param message + * @return R + */ + public R fail(String message) { + return R.fail(message); + } + + /** + * 返回ApiResult + * + * @param flag + * @return R + */ + public R status(boolean flag) { + return R.status(flag); + } + + + /**============================ FILE ================================================= */ + + /** + * 获取BladeFile封装类 + * + * @param file + * @return + */ + public LocalFile getFile(MultipartFile file) { + return BladeFileUtil.getFile(file); + } + + /** + * 获取BladeFile封装类 + * + * @param file + * @param dir + * @return + */ + public LocalFile getFile(MultipartFile file, String dir) { + return BladeFileUtil.getFile(file, dir); + } + + /** + * 获取BladeFile封装类 + * + * @param file + * @param dir + * @param path + * @param virtualPath + * @return + */ + public LocalFile getFile(MultipartFile file, String dir, String path, String virtualPath) { + return BladeFileUtil.getFile(file, dir, path, virtualPath); + } + + /** + * 获取BladeFile封装类 + * + * @param files + * @return + */ + public List getFiles(List files) { + return BladeFileUtil.getFiles(files); + } + + /** + * 获取BladeFile封装类 + * + * @param files + * @param dir + * @return + */ + public List getFiles(List files, String dir) { + return BladeFileUtil.getFiles(files, dir); + } + + /** + * 获取BladeFile封装类 + * + * @param files + * @param path + * @param virtualPath + * @return + */ + public List getFiles(List files, String dir, String path, String virtualPath) { + return BladeFileUtil.getFiles(files, dir, path, virtualPath); + } + /** + * 下载文件 + * + * @param file 文件 + * @return {ResponseEntity} + * @throws IOException io异常 + */ + protected ResponseEntity download(File file) throws IOException { + String fileName = file.getName(); + return download(file, fileName); + } + + /** + * 下载 + * + * @param file 文件 + * @param fileName 生成的文件名 + * @return {ResponseEntity} + * @throws IOException io异常 + */ + protected ResponseEntity download(File file, String fileName) throws IOException { + Resource resource = new FileSystemResource(file); + return download(resource, fileName); + } + + /** + * 下载 + * + * @param resource 资源 + * @param fileName 生成的文件名 + * @return {ResponseEntity} + * @throws IOException io异常 + */ + protected ResponseEntity download(Resource resource, String fileName) throws IOException { + HttpServletRequest request = WebUtil.getRequest(); + String header = request.getHeader(HttpHeaders.USER_AGENT); + // 避免空指针 + header = header == null ? StringPool.EMPTY : header.toUpperCase(); + HttpStatus status; + String msie= "MSIE"; + String trident= "TRIDENT"; + String edge= "EDGE"; + if (header.contains(msie) || header.contains(trident) || header.contains(edge)) { + status = HttpStatus.OK; + } else { + status = HttpStatus.CREATED; + } + // 断点续传 + long position = 0; + long count = resource.contentLength(); + String range = request.getHeader(HttpHeaders.RANGE); + if (null != range) { + status = HttpStatus.PARTIAL_CONTENT; + String[] rangeRange = range.replace("bytes=", StringPool.EMPTY).split(StringPool.DASH); + position = Long.parseLong(rangeRange[0]); + if (rangeRange.length > 1) { + long end = Long.parseLong(rangeRange[1]); + count = end - position + 1; + } + } + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_OCTET_STREAM); + String encodeFileName = UriUtils.encode(fileName, Charsets.UTF_8); + // 兼容各种浏览器下载: + // https://blog.robotshell.org/2012/deal-with-http-header-encoding-for-file-download/ + String disposition = "attachment;" + + "filename=\"" + encodeFileName + "\";" + + "filename*=utf-8''" + encodeFileName; + headers.set(HttpHeaders.CONTENT_DISPOSITION, disposition); + return new ResponseEntity<>(new ResourceRegion(resource, position, count), headers, status); + } + +} diff --git a/blade-core-boot/src/main/java/org/springblade/core/boot/error/ErrorType.java b/blade-core-boot/src/main/java/org/springblade/core/boot/error/ErrorType.java new file mode 100644 index 0000000..a988ca7 --- /dev/null +++ b/blade-core-boot/src/main/java/org/springblade/core/boot/error/ErrorType.java @@ -0,0 +1,66 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.boot.error; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.lang.Nullable; + +/** + * 异常类型 + * + * @author L.cm + */ +@Getter +@RequiredArgsConstructor +public enum ErrorType { + /** + * 异常类型 + */ + REQUEST("request"), + ASYNC("async"), + SCHEDULER("scheduler"), + WEB_SOCKET("websocket"), + OTHER("other"); + + @JsonValue + private final String type; + + @Nullable + @JsonCreator + public static ErrorType of(String type) { + ErrorType[] values = ErrorType.values(); + for (ErrorType errorType : values) { + if (errorType.type.equals(type)) { + return errorType; + } + } + return null; + } + +} diff --git a/blade-core-boot/src/main/java/org/springblade/core/boot/error/ErrorUtil.java b/blade-core-boot/src/main/java/org/springblade/core/boot/error/ErrorUtil.java new file mode 100644 index 0000000..b606a5f --- /dev/null +++ b/blade-core-boot/src/main/java/org/springblade/core/boot/error/ErrorUtil.java @@ -0,0 +1,62 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.boot.error; + +import org.springblade.core.log.model.LogError; +import org.springblade.core.tool.utils.DateUtil; +import org.springblade.core.tool.utils.Exceptions; +import org.springblade.core.tool.utils.ObjectUtil; + +/** + * 异常工具类 + * + * @author L.cm + */ +public class ErrorUtil { + + /** + * 初始化异常信息 + * + * @param error 异常 + * @param event 异常事件封装 + */ + public static void initErrorInfo(Throwable error, LogError event) { + // 堆栈信息 + event.setStackTrace(Exceptions.getStackTraceAsString(error)); + event.setExceptionName(error.getClass().getName()); + event.setMessage(error.getMessage()); + event.setCreateTime(DateUtil.now()); + StackTraceElement[] elements = error.getStackTrace(); + if (ObjectUtil.isNotEmpty(elements)) { + // 报错的类信息 + StackTraceElement element = elements[0]; + event.setMethodClass(element.getClassName()); + event.setFileName(element.getFileName()); + event.setMethodName(element.getMethodName()); + event.setLineNumber(element.getLineNumber()); + } + } +} diff --git a/blade-core-boot/src/main/java/org/springblade/core/boot/file/BladeFileUtil.java b/blade-core-boot/src/main/java/org/springblade/core/boot/file/BladeFileUtil.java new file mode 100644 index 0000000..460a68b --- /dev/null +++ b/blade-core-boot/src/main/java/org/springblade/core/boot/file/BladeFileUtil.java @@ -0,0 +1,249 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.boot.file; + +import org.springblade.core.tool.utils.StringPool; +import org.springblade.core.tool.utils.StringUtil; +import org.springframework.web.multipart.MultipartFile; + +import java.util.*; + +/** + * 文件工具类 + * + * @author Chill + */ +public class BladeFileUtil { + + /** + * 定义允许上传的文件扩展名 + */ + private static final HashMap EXT_MAP = new HashMap<>(); + private static final String IS_DIR = "is_dir"; + private static final String FILE_NAME = "filename"; + private static final String FILE_SIZE = "filesize"; + + /** + * 图片扩展名 + */ + private static final String[] FILE_TYPES = new String[]{"gif", "jpg", "jpeg", "png", "bmp"}; + + static { + EXT_MAP.put("image", ".gif,.jpg,.jpeg,.png,.bmp,.JPG,.JPEG,.PNG"); + EXT_MAP.put("flash", ".swf,.flv"); + EXT_MAP.put("media", ".swf,.flv,.mp3,.mp4,.wav,.wma,.wmv,.mid,.avi,.mpg,.asf,.rm,.rmvb"); + EXT_MAP.put("file", ".doc,.docx,.xls,.xlsx,.ppt,.htm,.html,.txt,.zip,.rar,.gz,.bz2"); + EXT_MAP.put("allfile", ".gif,.jpg,.jpeg,.png,.bmp,.swf,.flv,.mp3,.mp4,.wav,.wma,.wmv,.mid,.avi,.mpg,.asf,.rm,.rmvb,.doc,.docx,.xls,.xlsx,.ppt,.htm,.html,.txt,.zip,.rar,.gz,.bz2"); + } + + /** + * 获取文件后缀 + * + * @param fileName 文件名 + * @return String 返回后缀 + */ + public static String getFileExt(String fileName) { + return fileName.substring(fileName.lastIndexOf(StringPool.DOT)); + } + + /** + * 测试文件后缀 只让指定后缀的文件上传,像jsp,war,sh等危险的后缀禁止 + * + * @param dir 目录 + * @param fileName 文件名 + * @return 返回成功与否 + */ + public static boolean testExt(String dir, String fileName) { + String fileExt = getFileExt(fileName); + String ext = EXT_MAP.get(dir); + return StringUtil.isNotBlank(ext) && (ext.contains(fileExt.toLowerCase()) || ext.contains(fileExt.toUpperCase())); + } + + /** + * 文件管理排序 + */ + public enum FileSort { + + /** + * 大小 + */ + size, + + /** + * 类型 + */ + type, + + /** + * 名称 + */ + name; + + /** + * 文本排序转换成枚举 + * + * @param sort + * @return + */ + public static FileSort of(String sort) { + try { + return FileSort.valueOf(sort); + } catch (Exception e) { + return FileSort.name; + } + } + } + + public static class NameComparator implements Comparator { + @Override + public int compare(Object a, Object b) { + Hashtable hashA = (Hashtable) a; + Hashtable hashB = (Hashtable) b; + if (((Boolean) hashA.get(IS_DIR)) && !((Boolean) hashB.get(IS_DIR))) { + return -1; + } else if (!((Boolean) hashA.get(IS_DIR)) && ((Boolean) hashB.get(IS_DIR))) { + return 1; + } else { + return ((String) hashA.get(FILE_NAME)).compareTo((String) hashB.get(FILE_NAME)); + } + } + } + + public static class SizeComparator implements Comparator { + @Override + public int compare(Object a, Object b) { + Hashtable hashA = (Hashtable) a; + Hashtable hashB = (Hashtable) b; + if (((Boolean) hashA.get(IS_DIR)) && !((Boolean) hashB.get(IS_DIR))) { + return -1; + } else if (!((Boolean) hashA.get(IS_DIR)) && ((Boolean) hashB.get(IS_DIR))) { + return 1; + } else { + if (((Long) hashA.get(FILE_SIZE)) > ((Long) hashB.get(FILE_SIZE))) { + return 1; + } else if (((Long) hashA.get(FILE_SIZE)) < ((Long) hashB.get(FILE_SIZE))) { + return -1; + } else { + return 0; + } + } + } + } + + public static class TypeComparator implements Comparator { + @Override + public int compare(Object a, Object b) { + Hashtable hashA = (Hashtable) a; + Hashtable hashB = (Hashtable) b; + if (((Boolean) hashA.get(IS_DIR)) && !((Boolean) hashB.get(IS_DIR))) { + return -1; + } else if (!((Boolean) hashA.get(IS_DIR)) && ((Boolean) hashB.get(IS_DIR))) { + return 1; + } else { + return ((String) hashA.get("filetype")).compareTo((String) hashB.get("filetype")); + } + } + } + + public static String formatUrl(String url) { + return url.replaceAll("\\\\", "/"); + } + + + /********************************BladeFile封装********************************************************/ + + /** + * 获取BladeFile封装类 + * + * @param file 文件 + * @return BladeFile + */ + public static LocalFile getFile(MultipartFile file) { + return getFile(file, "image", null, null); + } + + /** + * 获取BladeFile封装类 + * + * @param file 文件 + * @param dir 目录 + * @return BladeFile + */ + public static LocalFile getFile(MultipartFile file, String dir) { + return getFile(file, dir, null, null); + } + + /** + * 获取BladeFile封装类 + * + * @param file 文件 + * @param dir 目录 + * @param path 路径 + * @param virtualPath 虚拟路径 + * @return BladeFile + */ + public static LocalFile getFile(MultipartFile file, String dir, String path, String virtualPath) { + return new LocalFile(file, dir, path, virtualPath); + } + + /** + * 获取BladeFile封装类 + * + * @param files 文件集合 + * @return BladeFile + */ + public static List getFiles(List files) { + return getFiles(files, "image", null, null); + } + + /** + * 获取BladeFile封装类 + * + * @param files 文件集合 + * @param dir 目录 + * @return BladeFile + */ + public static List getFiles(List files, String dir) { + return getFiles(files, dir, null, null); + } + + /** + * 获取BladeFile封装类 + * + * @param files 文件集合 + * @param path 路径 + * @param virtualPath 虚拟路径 + * @return BladeFile + */ + public static List getFiles(List files, String dir, String path, String virtualPath) { + List list = new ArrayList<>(); + for (MultipartFile file : files) { + list.add(new LocalFile(file, dir, path, virtualPath)); + } + return list; + } + +} diff --git a/blade-core-boot/src/main/java/org/springblade/core/boot/file/FileProxyManager.java b/blade-core-boot/src/main/java/org/springblade/core/boot/file/FileProxyManager.java new file mode 100644 index 0000000..264059c --- /dev/null +++ b/blade-core-boot/src/main/java/org/springblade/core/boot/file/FileProxyManager.java @@ -0,0 +1,60 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.boot.file; + +import java.io.File; + +/** + * 文件管理类 + * + * @author Chill + */ +public class FileProxyManager { + private IFileProxy defaultFileProxyFactory = new LocalFileProxyFactory(); + + private static final FileProxyManager ME = new FileProxyManager(); + + public static FileProxyManager me() { + return ME; + } + + public IFileProxy getDefaultFileProxyFactory() { + return defaultFileProxyFactory; + } + + public void setDefaultFileProxyFactory(IFileProxy defaultFileProxyFactory) { + this.defaultFileProxyFactory = defaultFileProxyFactory; + } + + public String[] path(File file, String dir) { + return defaultFileProxyFactory.path(file, dir); + } + + public File rename(File file, String path) { + return defaultFileProxyFactory.rename(file, path); + } + +} diff --git a/blade-core-boot/src/main/java/org/springblade/core/boot/file/IFileProxy.java b/blade-core-boot/src/main/java/org/springblade/core/boot/file/IFileProxy.java new file mode 100644 index 0000000..12de3f9 --- /dev/null +++ b/blade-core-boot/src/main/java/org/springblade/core/boot/file/IFileProxy.java @@ -0,0 +1,62 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.boot.file; + +import java.io.File; + +/** + * 文件代理接口 + * + * @author Chill + */ +public interface IFileProxy { + + /** + * 返回路径[物理路径][虚拟路径] + * + * @param file 文件 + * @param dir 目录 + * @return + */ + String[] path(File file, String dir); + + /** + * 文件重命名策略 + * + * @param file 文件 + * @param path 路径 + * @return + */ + File rename(File file, String path); + + /** + * 图片压缩 + * + * @param path 路径 + */ + void compress(String path); + +} diff --git a/blade-core-boot/src/main/java/org/springblade/core/boot/file/LocalFile.java b/blade-core-boot/src/main/java/org/springblade/core/boot/file/LocalFile.java new file mode 100644 index 0000000..48384ee --- /dev/null +++ b/blade-core-boot/src/main/java/org/springblade/core/boot/file/LocalFile.java @@ -0,0 +1,166 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.boot.file; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Data; +import org.springblade.core.boot.props.BladeFileProperties; +import org.springblade.core.tool.utils.DateUtil; +import org.springblade.core.tool.utils.SpringUtil; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.IOException; + +/** + * 上传文件封装 + * + * @author Chill + */ +@Data +public class LocalFile { + /** + * 上传文件在附件表中的id + */ + private Object fileId; + + /** + * 上传文件 + */ + @JsonIgnore + private MultipartFile file; + + /** + * 文件外网地址 + */ + private String domain; + + /** + * 上传分类文件夹 + */ + private String dir; + + /** + * 上传物理路径 + */ + private String uploadPath; + + /** + * 上传虚拟路径 + */ + private String uploadVirtualPath; + + /** + * 文件名 + */ + private String fileName; + + /** + * 真实文件名 + */ + private String originalFileName; + + /** + * 文件配置 + */ + private static BladeFileProperties fileProperties; + + private static BladeFileProperties getBladeFileProperties() { + if (fileProperties == null) { + fileProperties = SpringUtil.getBean(BladeFileProperties.class); + } + return fileProperties; + } + + public LocalFile(MultipartFile file, String dir) { + this.dir = dir; + this.file = file; + this.fileName = file.getName(); + this.originalFileName = file.getOriginalFilename(); + this.domain = getBladeFileProperties().getUploadDomain(); + this.uploadPath = BladeFileUtil.formatUrl(File.separator + getBladeFileProperties().getUploadRealPath() + File.separator + dir + File.separator + DateUtil.format(DateUtil.now(), "yyyyMMdd") + File.separator + this.originalFileName); + this.uploadVirtualPath = BladeFileUtil.formatUrl(getBladeFileProperties().getUploadCtxPath().replace(getBladeFileProperties().getContextPath(), "") + File.separator + dir + File.separator + DateUtil.format(DateUtil.now(), "yyyyMMdd") + File.separator + this.originalFileName); + } + + public LocalFile(MultipartFile file, String dir, String uploadPath, String uploadVirtualPath) { + this(file, dir); + if (null != uploadPath) { + this.uploadPath = BladeFileUtil.formatUrl(uploadPath); + this.uploadVirtualPath = BladeFileUtil.formatUrl(uploadVirtualPath); + } + } + + /** + * 图片上传 + */ + public void transfer() { + transfer(getBladeFileProperties().getCompress()); + } + + /** + * 图片上传 + * + * @param compress 是否压缩 + */ + public void transfer(boolean compress) { + IFileProxy fileFactory = FileProxyManager.me().getDefaultFileProxyFactory(); + this.transfer(fileFactory, compress); + } + + /** + * 图片上传 + * + * @param fileFactory 文件上传工厂类 + * @param compress 是否压缩 + */ + public void transfer(IFileProxy fileFactory, boolean compress) { + try { + File file = new File(uploadPath); + + if (null != fileFactory) { + String[] path = fileFactory.path(file, dir); + this.uploadPath = path[0]; + this.uploadVirtualPath = path[1]; + file = fileFactory.rename(file, path[0]); + } + + File pfile = file.getParentFile(); + if (!pfile.exists()) { + pfile.mkdirs(); + } + + this.file.transferTo(file); + + if (compress) { + fileFactory.compress(this.uploadPath); + } + + } catch (IllegalStateException | IOException e) { + e.printStackTrace(); + } + } + +} diff --git a/blade-core-boot/src/main/java/org/springblade/core/boot/file/LocalFileProxyFactory.java b/blade-core-boot/src/main/java/org/springblade/core/boot/file/LocalFileProxyFactory.java new file mode 100644 index 0000000..034736f --- /dev/null +++ b/blade-core-boot/src/main/java/org/springblade/core/boot/file/LocalFileProxyFactory.java @@ -0,0 +1,126 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.boot.file; + +import org.springblade.core.boot.props.BladeFileProperties; +import org.springblade.core.tool.utils.DateUtil; +import org.springblade.core.tool.utils.ImageUtil; +import org.springblade.core.tool.utils.SpringUtil; +import org.springblade.core.tool.utils.StringPool; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; + +/** + * 文件代理类 + * + * @author Chill + */ +public class LocalFileProxyFactory implements IFileProxy { + + /** + * 文件配置 + */ + private static BladeFileProperties fileProperties; + + private static BladeFileProperties getBladeFileProperties() { + if (fileProperties == null) { + fileProperties = SpringUtil.getBean(BladeFileProperties.class); + } + return fileProperties; + } + + @Override + public File rename(File f, String path) { + File dest = new File(path); + f.renameTo(dest); + return dest; + } + + @Override + public String[] path(File f, String dir) { + //避免网络延迟导致时间不同步 + long time = System.nanoTime(); + + StringBuilder uploadPath = new StringBuilder() + .append(getFileDir(dir, getBladeFileProperties().getUploadRealPath())) + .append(time) + .append(getFileExt(f.getName())); + + StringBuilder virtualPath = new StringBuilder() + .append(getFileDir(dir, getBladeFileProperties().getUploadCtxPath())) + .append(time) + .append(getFileExt(f.getName())); + + return new String[]{BladeFileUtil.formatUrl(uploadPath.toString()), BladeFileUtil.formatUrl(virtualPath.toString())}; + } + + /** + * 获取文件后缀 + * + * @param fileName 文件名 + * @return 文件后缀 + */ + public static String getFileExt(String fileName) { + if (!fileName.contains(StringPool.DOT)) { + return ".jpg"; + } else { + return fileName.substring(fileName.lastIndexOf(StringPool.DOT)); + } + } + + /** + * 获取文件保存地址 + * + * @param dir 目录 + * @param saveDir 保存目录 + * @return 地址 + */ + public static String getFileDir(String dir, String saveDir) { + StringBuilder newFileDir = new StringBuilder(); + newFileDir.append(saveDir) + .append(File.separator).append(dir).append(File.separator).append(DateUtil.format(DateUtil.now(), "yyyyMMdd")) + .append(File.separator); + return newFileDir.toString(); + } + + + /** + * 图片压缩 + * + * @param path 文件地址 + */ + @Override + public void compress(String path) { + try { + ImageUtil.zoomScale(ImageUtil.readImage(path), new FileOutputStream(new File(path)), null, getBladeFileProperties().getCompressScale(), getBladeFileProperties().getCompressFlag()); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } + } + +} diff --git a/blade-core-boot/src/main/java/org/springblade/core/boot/props/BladeFileProperties.java b/blade-core-boot/src/main/java/org/springblade/core/boot/props/BladeFileProperties.java new file mode 100644 index 0000000..21c8368 --- /dev/null +++ b/blade-core-boot/src/main/java/org/springblade/core/boot/props/BladeFileProperties.java @@ -0,0 +1,101 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.boot.props; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * BladeFileProperties + * + * @author Chill + */ +@Getter +@Setter +@ConfigurationProperties("blade.file") +public class BladeFileProperties { + + /** + * 远程上传模式 + */ + private boolean remoteMode = false; + + /** + * 外网地址 + */ + private String uploadDomain = "http://127.0.0.1:8999"; + + /** + * 上传下载路径(物理路径) + */ + private String remotePath = System.getProperty("user.dir") + "/target/blade"; + + /** + * 上传路径(相对路径) + */ + private String uploadPath = "/upload"; + + /** + * 下载路径 + */ + private String downloadPath = "/download"; + + /** + * 图片压缩 + */ + private Boolean compress = false; + + /** + * 图片压缩比例 + */ + private Double compressScale = 2.00; + + /** + * 图片缩放选择:true放大;false缩小 + */ + private Boolean compressFlag = false; + + /** + * 项目物理路径 + */ + private String realPath = System.getProperty("user.dir"); + + /** + * 项目相对路径 + */ + private String contextPath = "/"; + + + public String getUploadRealPath() { + return (remoteMode ? remotePath : realPath) + uploadPath; + } + + public String getUploadCtxPath() { + return contextPath + uploadPath; + } + +} diff --git a/blade-core-boot/src/main/java/org/springblade/core/boot/props/BladeUploadProperties.java b/blade-core-boot/src/main/java/org/springblade/core/boot/props/BladeUploadProperties.java new file mode 100644 index 0000000..b3339f2 --- /dev/null +++ b/blade-core-boot/src/main/java/org/springblade/core/boot/props/BladeUploadProperties.java @@ -0,0 +1,52 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.boot.props; + +import lombok.Getter; +import lombok.Setter; +import org.springblade.core.tool.utils.PathUtil; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.lang.Nullable; + + +/** + * 文件上传配置 + * + * @author Chill + */ +@Getter +@Setter +@RefreshScope +@ConfigurationProperties("blade.upload") +public class BladeUploadProperties { + + /** + * 文件保存目录,默认:jar 包同级目录 + */ + @Nullable + private String savePath = PathUtil.getJarPath(); +} diff --git a/blade-core-boot/src/main/java/org/springblade/core/boot/request/BladeHttpServletRequestWrapper.java b/blade-core-boot/src/main/java/org/springblade/core/boot/request/BladeHttpServletRequestWrapper.java new file mode 100644 index 0000000..ac0f9ab --- /dev/null +++ b/blade-core-boot/src/main/java/org/springblade/core/boot/request/BladeHttpServletRequestWrapper.java @@ -0,0 +1,129 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.boot.request; + +import org.springblade.core.tool.utils.WebUtil; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; + +import jakarta.servlet.ReadListener; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; + +/** + * 全局Request包装 + * + * @author Chill + */ +public class BladeHttpServletRequestWrapper extends HttpServletRequestWrapper { + + /** + * 没被包装过的HttpServletRequest(特殊场景,需要自己过滤) + */ + private final HttpServletRequest orgRequest; + /** + * 缓存报文,支持多次读取流 + */ + private byte[] body; + + + public BladeHttpServletRequestWrapper(HttpServletRequest request) { + super(request); + orgRequest = request; + } + + @Override + public BufferedReader getReader() throws IOException { + return new BufferedReader(new InputStreamReader(getInputStream())); + } + + @Override + public ServletInputStream getInputStream() throws IOException { + if (super.getHeader(HttpHeaders.CONTENT_TYPE) == null) { + return super.getInputStream(); + } + + if (super.getHeader(HttpHeaders.CONTENT_TYPE).startsWith(MediaType.MULTIPART_FORM_DATA_VALUE)) { + return super.getInputStream(); + } + + if (body == null) { + body = WebUtil.getRequestBody(super.getInputStream()).getBytes(); + } + + final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body); + + return new ServletInputStream() { + + @Override + public int read() { + return byteArrayInputStream.read(); + } + + @Override + public boolean isFinished() { + return false; + } + + @Override + public boolean isReady() { + return false; + } + + @Override + public void setReadListener(ReadListener readListener) { + } + }; + } + + /** + * 获取初始request + * + * @return HttpServletRequest + */ + public HttpServletRequest getOrgRequest() { + return orgRequest; + } + + /** + * 获取初始request + * + * @param request request + * @return HttpServletRequest + */ + public static HttpServletRequest getOrgRequest(HttpServletRequest request) { + if (request instanceof BladeHttpServletRequestWrapper) { + return ((BladeHttpServletRequestWrapper) request).getOrgRequest(); + } + return request; + } + +} diff --git a/blade-core-boot/src/main/java/org/springblade/core/boot/request/BladeRequestFilter.java b/blade-core-boot/src/main/java/org/springblade/core/boot/request/BladeRequestFilter.java new file mode 100644 index 0000000..8a2e964 --- /dev/null +++ b/blade-core-boot/src/main/java/org/springblade/core/boot/request/BladeRequestFilter.java @@ -0,0 +1,84 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.boot.request; + +import lombok.AllArgsConstructor; +import org.springframework.util.AntPathMatcher; + +import jakarta.servlet.*; +import jakarta.servlet.http.HttpServletRequest; +import java.io.IOException; + +/** + * Request全局过滤 + * + * @author Chill + */ +@AllArgsConstructor +public class BladeRequestFilter implements Filter { + + private final RequestProperties requestProperties; + private final XssProperties xssProperties; + private final AntPathMatcher antPathMatcher = new AntPathMatcher(); + + @Override + public void init(FilterConfig config) { + + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + String path = ((HttpServletRequest) request).getServletPath(); + // 跳过 Request 包装 + if (!requestProperties.getEnabled() || isRequestSkip(path)) { + chain.doFilter(request, response); + } + // 默认 Request 包装 + else if (!xssProperties.getEnabled() || isXssSkip(path)) { + BladeHttpServletRequestWrapper bladeRequest = new BladeHttpServletRequestWrapper((HttpServletRequest) request); + chain.doFilter(bladeRequest, response); + } + // Xss Request 包装 + else { + XssHttpServletRequestWrapper xssRequest = new XssHttpServletRequestWrapper((HttpServletRequest) request); + chain.doFilter(xssRequest, response); + } + } + + private boolean isRequestSkip(String path) { + return requestProperties.getSkipUrl().stream().anyMatch(pattern -> antPathMatcher.match(pattern, path)); + } + + private boolean isXssSkip(String path) { + return xssProperties.getSkipUrl().stream().anyMatch(pattern -> antPathMatcher.match(pattern, path)); + } + + @Override + public void destroy() { + + } + +} diff --git a/blade-core-boot/src/main/java/org/springblade/core/boot/request/RequestProperties.java b/blade-core-boot/src/main/java/org/springblade/core/boot/request/RequestProperties.java new file mode 100644 index 0000000..c97c486 --- /dev/null +++ b/blade-core-boot/src/main/java/org/springblade/core/boot/request/RequestProperties.java @@ -0,0 +1,53 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.boot.request; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.ArrayList; +import java.util.List; + +/** + * Request配置类 + * + * @author Chill + */ +@Data +@ConfigurationProperties("blade.request") +public class RequestProperties { + + /** + * 开启自定义request + */ + private Boolean enabled = true; + + /** + * 放行url + */ + private List skipUrl = new ArrayList<>(); + +} diff --git a/blade-core-boot/src/main/java/org/springblade/core/boot/request/XssHtmlFilter.java b/blade-core-boot/src/main/java/org/springblade/core/boot/request/XssHtmlFilter.java new file mode 100644 index 0000000..47a89e5 --- /dev/null +++ b/blade-core-boot/src/main/java/org/springblade/core/boot/request/XssHtmlFilter.java @@ -0,0 +1,536 @@ +package org.springblade.core.boot.request; + +import org.springblade.core.tool.utils.StringPool; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * HTML filtering utility for protecting against XSS (Cross Site Scripting). + *

+ * This code is licensed LGPLv3 + *

+ * This code is a Java port of the original work in PHP by Cal Hendersen. + * http://code.iamcal.com/php/lib_filter/ + *

+ * The trickiest part of the translation was handling the differences in regex handling + * between PHP and Java. These resources were helpful in the process: + *

+ * http://java.sun.com/j2se/1.4.2/docs/api/java/util/regex/Pattern.html + * http://us2.php.net/manual/en/reference.pcre.pattern.modifiers.php + * http://www.regular-expressions.info/modifiers.html + *

+ * A note on naming conventions: instance variables are prefixed with a "v"; global + * constants are in all caps. + *

+ * Sample use: + * String input = ... + * String clean = new HtmlFilter().filter( input ); + *

+ * The class is not thread safe. Create a new instance if in doubt. + *

+ * If you find bugs or have suggestions on improvement (especially regarding + * performance), please contact us. The latest version of this + * source, and our contact details, can be found at http://xss-html-filter.sf.net + * + * @author Joseph O'Connell + * @author Cal Hendersen + * @author Michael Semb Wever + */ +public final class XssHtmlFilter { + + /** + * regex flag union representing /si modifiers in php + **/ + private static final int REGEX_FLAGS_SI = Pattern.CASE_INSENSITIVE | Pattern.DOTALL; + private static final Pattern P_COMMENTS = Pattern.compile("", Pattern.DOTALL); + private static final Pattern P_COMMENT = Pattern.compile("^!--(.*)--$", REGEX_FLAGS_SI); + private static final Pattern P_TAGS = Pattern.compile("<(.*?)>", Pattern.DOTALL); + private static final Pattern P_END_TAG = Pattern.compile("^/([a-z0-9]+)", REGEX_FLAGS_SI); + private static final Pattern P_START_TAG = Pattern.compile("^([a-z0-9]+)(.*?)(/?)$", REGEX_FLAGS_SI); + private static final Pattern P_QUOTED_ATTRIBUTES = Pattern.compile("([a-z0-9]+)=([\"'])(.*?)\\2", REGEX_FLAGS_SI); + private static final Pattern P_UNQUOTED_ATTRIBUTES = Pattern.compile("([a-z0-9]+)(=)([^\"\\s']+)", REGEX_FLAGS_SI); + private static final Pattern P_PROTOCOL = Pattern.compile("^([^:]+):", REGEX_FLAGS_SI); + private static final Pattern P_ENTITY = Pattern.compile("&#(\\d+);?"); + private static final Pattern P_ENTITY_UNICODE = Pattern.compile("&#x([0-9a-f]+);?"); + private static final Pattern P_ENCODE = Pattern.compile("%([0-9a-f]{2});?"); + private static final Pattern P_VALID_ENTITIES = Pattern.compile("&([^&;]*)(?=(;|&|$))"); + private static final Pattern P_VALID_QUOTES = Pattern.compile("(>|^)([^<]+?)(<|$)", Pattern.DOTALL); + private static final Pattern P_END_ARROW = Pattern.compile("^>"); + private static final Pattern P_BODY_TO_END = Pattern.compile("<([^>]*?)(?=<|$)"); + private static final Pattern P_XML_CONTENT = Pattern.compile("(^|>)([^<]*?)(?=>)"); + private static final Pattern P_STRAY_LEFT_ARROW = Pattern.compile("<([^>]*?)(?=<|$)"); + private static final Pattern P_STRAY_RIGHT_ARROW = Pattern.compile("(^|>)([^<]*?)(?=>)"); + private static final Pattern P_AMP = Pattern.compile("&"); + private static final Pattern P_QUOTE = Pattern.compile("”"); + private static final Pattern P_LEFT_ARROW = Pattern.compile("<"); + private static final Pattern P_RIGHT_ARROW = Pattern.compile(">"); + private static final Pattern P_BOTH_ARROWS = Pattern.compile("<>"); + + + private static final ConcurrentMap P_REMOVE_PAIR_BLANKS = new ConcurrentHashMap(); + private static final ConcurrentMap P_REMOVE_SELF_BLANKS = new ConcurrentHashMap(); + + /** + * set of allowed html elements, along with allowed attributes for each element + **/ + private final Map> vAllowed; + /** + * counts of open tags for each (allowable) html element + **/ + private final Map vTagCounts = new HashMap(); + + /** + * html elements which must always be self-closing (e.g. "") + **/ + private final String[] vSelfClosingTags; + /** + * html elements which must always have separate opening and closing tags (e.g. "") + **/ + private final String[] vNeedClosingTags; + /** + * set of disallowed html elements + **/ + private final String[] vDisallowed; + /** + * attributes which should be checked for valid protocols + **/ + private final String[] vProtocolAtts; + /** + * allowed protocols + **/ + private final String[] vAllowedProtocols; + /** + * tags which should be removed if they contain no content (e.g. "" or "") + **/ + private final String[] vRemoveBlanks; + /** + * entities allowed within html markup + **/ + private final String[] vAllowedEntities; + /** + * flag determining whether comments are allowed in input String. + */ + private final boolean stripComment; + private final boolean encodeQuotes; + private boolean vDebug = false; + /** + * flag determining whether to try to make tags when presented with "unbalanced" + * angle brackets (e.g. "" becomes " text "). If set to false, + * unbalanced angle brackets will be html escaped. + */ + private final boolean alwaysMakeTags; + + /** + * Default constructor. + */ + public XssHtmlFilter() { + vAllowed = new HashMap<>(); + + final ArrayList aAtts = new ArrayList(); + aAtts.add("href"); + aAtts.add("target"); + vAllowed.put("a", aAtts); + + final ArrayList imgAtts = new ArrayList(); + imgAtts.add("src"); + imgAtts.add("width"); + imgAtts.add("height"); + imgAtts.add("alt"); + vAllowed.put("img", imgAtts); + + final ArrayList noAtts = new ArrayList(); + vAllowed.put("b", noAtts); + vAllowed.put("strong", noAtts); + vAllowed.put("i", noAtts); + vAllowed.put("em", noAtts); + + vSelfClosingTags = new String[]{"img"}; + vNeedClosingTags = new String[]{"a", "b", "strong", "i", "em"}; + vDisallowed = new String[]{}; + vAllowedProtocols = new String[]{"http", "mailto", "https"}; + vProtocolAtts = new String[]{"src", "href"}; + vRemoveBlanks = new String[]{"a", "b", "strong", "i", "em"}; + vAllowedEntities = new String[]{"amp", "gt", "lt", "quot"}; + stripComment = true; + encodeQuotes = true; + alwaysMakeTags = false; + } + + /** + * Set debug flag to true. Otherwise use default settings. See the default constructor. + * + * @param debug turn debug on with a true argument + */ + public XssHtmlFilter(final boolean debug) { + this(); + vDebug = debug; + + } + + /** + * Map-parameter configurable constructor. + * + * @param conf map containing configuration. keys match field names. + */ + public XssHtmlFilter(final Map conf) { + + assert conf.containsKey("vAllowed") : "configuration requires vAllowed"; + assert conf.containsKey("vSelfClosingTags") : "configuration requires vSelfClosingTags"; + assert conf.containsKey("vNeedClosingTags") : "configuration requires vNeedClosingTags"; + assert conf.containsKey("vDisallowed") : "configuration requires vDisallowed"; + assert conf.containsKey("vAllowedProtocols") : "configuration requires vAllowedProtocols"; + assert conf.containsKey("vProtocolAtts") : "configuration requires vProtocolAtts"; + assert conf.containsKey("vRemoveBlanks") : "configuration requires vRemoveBlanks"; + assert conf.containsKey("vAllowedEntities") : "configuration requires vAllowedEntities"; + + vAllowed = Collections.unmodifiableMap((HashMap>) conf.get("vAllowed")); + vSelfClosingTags = (String[]) conf.get("vSelfClosingTags"); + vNeedClosingTags = (String[]) conf.get("vNeedClosingTags"); + vDisallowed = (String[]) conf.get("vDisallowed"); + vAllowedProtocols = (String[]) conf.get("vAllowedProtocols"); + vProtocolAtts = (String[]) conf.get("vProtocolAtts"); + vRemoveBlanks = (String[]) conf.get("vRemoveBlanks"); + vAllowedEntities = (String[]) conf.get("vAllowedEntities"); + stripComment = conf.containsKey("stripComment") ? (Boolean) conf.get("stripComment") : true; + encodeQuotes = conf.containsKey("encodeQuotes") ? (Boolean) conf.get("encodeQuotes") : true; + alwaysMakeTags = conf.containsKey("alwaysMakeTags") ? (Boolean) conf.get("alwaysMakeTags") : true; + } + + private void reset() { + vTagCounts.clear(); + } + + private void debug(final String msg) { + if (vDebug) { + Logger.getAnonymousLogger().info(msg); + } + } + + public static String chr(final int decimal) { + return String.valueOf((char) decimal); + } + + public static String htmlSpecialChars(final String s) { + String result = s; + result = regexReplace(P_AMP, "&", result); + result = regexReplace(P_QUOTE, """, result); + result = regexReplace(P_LEFT_ARROW, "<", result); + result = regexReplace(P_RIGHT_ARROW, ">", result); + return result; + } + + //--------------------------------------------------------------- + + /** + * given a user submitted input String, filter out any invalid or restricted + * html. + * + * @param input text (i.e. submitted by a user) than may contain html + * @return "clean" version of input, with only valid, whitelisted html elements allowed + */ + public String filter(final String input) { + reset(); + String s = input; + + debug("************************************************"); + debug(" INPUT: " + input); + + s = escapeComments(s); + debug(" escapeComments: " + s); + + s = balanceHtml(s); + debug(" balanceHtml: " + s); + + s = checkTags(s); + debug(" checkTags: " + s); + + s = processRemoveBlanks(s); + debug("processRemoveBlanks: " + s); + + s = validateEntities(s); + debug(" validateEntites: " + s); + + debug("************************************************\n\n"); + return s; + } + + public boolean isAlwaysMakeTags() { + return alwaysMakeTags; + } + + public boolean isStripComments() { + return stripComment; + } + + private String escapeComments(final String s) { + final Matcher m = P_COMMENTS.matcher(s); + final StringBuffer buf = new StringBuffer(); + if (m.find()) { + final String match = m.group(1); + m.appendReplacement(buf, Matcher.quoteReplacement("")); + } + m.appendTail(buf); + + return buf.toString(); + } + + private String balanceHtml(String s) { + if (alwaysMakeTags) { + // + // try and form html + // + s = regexReplace(P_END_ARROW, "", s); + s = regexReplace(P_BODY_TO_END, "<$1>", s); + s = regexReplace(P_XML_CONTENT, "$1<$2", s); + + } else { + // + // escape stray brackets + // + s = regexReplace(P_STRAY_LEFT_ARROW, "<$1", s); + s = regexReplace(P_STRAY_RIGHT_ARROW, "$1$2><", s); + + // + // the last regexp causes '<>' entities to appear + // (we need to do a lookahead assertion so that the last bracket can + // be used in the next pass of the regexp) + // + s = regexReplace(P_BOTH_ARROWS, "", s); + } + + return s; + } + + private String checkTags(String s) { + Matcher m = P_TAGS.matcher(s); + + final StringBuffer buf = new StringBuffer(); + while (m.find()) { + String replaceStr = m.group(1); + replaceStr = processTag(replaceStr); + m.appendReplacement(buf, Matcher.quoteReplacement(replaceStr)); + } + m.appendTail(buf); + + s = buf.toString(); + + // these get tallied in processTag + // (remember to reset before subsequent calls to filter method) + for (String key : vTagCounts.keySet()) { + for (int ii = 0; ii < vTagCounts.get(key); ii++) { + s += ""; + } + } + + return s; + } + + private String processRemoveBlanks(final String s) { + String result = s; + for (String tag : vRemoveBlanks) { + if (!P_REMOVE_PAIR_BLANKS.containsKey(tag)) { + P_REMOVE_PAIR_BLANKS.putIfAbsent(tag, Pattern.compile("<" + tag + "(\\s[^>]*)?>")); + } + result = regexReplace(P_REMOVE_PAIR_BLANKS.get(tag), "", result); + if (!P_REMOVE_SELF_BLANKS.containsKey(tag)) { + P_REMOVE_SELF_BLANKS.putIfAbsent(tag, Pattern.compile("<" + tag + "(\\s[^>]*)?/>")); + } + result = regexReplace(P_REMOVE_SELF_BLANKS.get(tag), "", result); + } + + return result; + } + + private static String regexReplace(final Pattern regexPattern, final String replacement, final String s) { + Matcher m = regexPattern.matcher(s); + return m.replaceAll(replacement); + } + + private String processTag(final String s) { + Matcher m = P_END_TAG.matcher(s); + if (m.find()) { + final String name = m.group(1).toLowerCase(); + if (allowed(name)) { + if (!inArray(name, vSelfClosingTags)) { + if (vTagCounts.containsKey(name)) { + vTagCounts.put(name, vTagCounts.get(name) - 1); + return ""; + } + } + } + } + m = P_START_TAG.matcher(s); + if (m.find()) { + final String name = m.group(1).toLowerCase(); + final String body = m.group(2); + String ending = m.group(3); + if (allowed(name)) { + String params = ""; + final Matcher m2 = P_QUOTED_ATTRIBUTES.matcher(body); + final Matcher m3 = P_UNQUOTED_ATTRIBUTES.matcher(body); + final List paramNames = new ArrayList(); + final List paramValues = new ArrayList(); + while (m2.find()) { + paramNames.add(m2.group(1)); + paramValues.add(m2.group(3)); + } + while (m3.find()) { + paramNames.add(m3.group(1)); + paramValues.add(m3.group(3)); + } + String paramName, paramValue; + for (int ii = 0; ii < paramNames.size(); ii++) { + paramName = paramNames.get(ii).toLowerCase(); + paramValue = paramValues.get(ii); + if (allowedAttribute(name, paramName)) { + if (inArray(paramName, vProtocolAtts)) { + paramValue = processParamProtocol(paramValue); + } + params += " " + paramName + "=\"" + paramValue + "\""; + } + } + if (inArray(name, vSelfClosingTags)) { + ending = " /"; + } + if (inArray(name, vNeedClosingTags)) { + ending = ""; + } + if (ending == null || ending.length() < 1) { + if (vTagCounts.containsKey(name)) { + vTagCounts.put(name, vTagCounts.get(name) + 1); + } else { + vTagCounts.put(name, 1); + } + } else { + ending = " /"; + } + return "<" + name + params + ending + ">"; + } else { + return ""; + } + } + m = P_COMMENT.matcher(s); + if (!stripComment && m.find()) { + return "<" + m.group() + ">"; + } + return ""; + } + + private String processParamProtocol(String s) { + s = decodeEntities(s); + final Matcher m = P_PROTOCOL.matcher(s); + if (m.find()) { + final String protocol = m.group(1); + if (!inArray(protocol, vAllowedProtocols)) { + // bad protocol, turn into local anchor link instead + s = "#" + s.substring(protocol.length() + 1); + if (s.startsWith(StringPool.DOUBLE_SLASH)) { + s = "#" + s.substring(3); + } + } + } + + return s; + } + + private String decodeEntities(String s) { + StringBuffer buf = new StringBuffer(); + + Matcher m = P_ENTITY.matcher(s); + while (m.find()) { + final String match = m.group(1); + final int decimal = Integer.decode(match); + m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal))); + } + m.appendTail(buf); + s = buf.toString(); + + buf = new StringBuffer(); + m = P_ENTITY_UNICODE.matcher(s); + while (m.find()) { + final String match = m.group(1); + final int decimal = Integer.valueOf(match, 16).intValue(); + m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal))); + } + m.appendTail(buf); + s = buf.toString(); + + buf = new StringBuffer(); + m = P_ENCODE.matcher(s); + while (m.find()) { + final String match = m.group(1); + final int decimal = Integer.valueOf(match, 16).intValue(); + m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal))); + } + m.appendTail(buf); + s = buf.toString(); + + s = validateEntities(s); + return s; + } + + private String validateEntities(final String s) { + StringBuffer buf = new StringBuffer(); + + // validate entities throughout the string + Matcher m = P_VALID_ENTITIES.matcher(s); + while (m.find()) { + final String one = m.group(1); + final String two = m.group(2); + m.appendReplacement(buf, Matcher.quoteReplacement(checkEntity(one, two))); + } + m.appendTail(buf); + + return encodeQuotes(buf.toString()); + } + + private String encodeQuotes(final String s) { + if (encodeQuotes) { + StringBuffer buf = new StringBuffer(); + Matcher m = P_VALID_QUOTES.matcher(s); + while (m.find()) { + final String one = m.group(1); + final String two = m.group(2); + final String three = m.group(3); + m.appendReplacement(buf, Matcher.quoteReplacement(one + regexReplace(P_QUOTE, """, two) + three)); + } + m.appendTail(buf); + return buf.toString(); + } else { + return s; + } + } + + private String checkEntity(final String preamble, final String term) { + + return ";".equals(term) && isValidEntity(preamble) + ? '&' + preamble + : "&" + preamble; + } + + private boolean isValidEntity(final String entity) { + return inArray(entity, vAllowedEntities); + } + + private static boolean inArray(final String s, final String[] array) { + for (String item : array) { + if (item != null && item.equals(s)) { + return true; + } + } + return false; + } + + private boolean allowed(final String name) { + return (vAllowed.isEmpty() || vAllowed.containsKey(name)) && !inArray(name, vDisallowed); + } + + private boolean allowedAttribute(final String name, final String paramName) { + return allowed(name) && (vAllowed.isEmpty() || vAllowed.get(name).contains(paramName)); + } +} diff --git a/blade-core-boot/src/main/java/org/springblade/core/boot/request/XssHttpServletRequestWrapper.java b/blade-core-boot/src/main/java/org/springblade/core/boot/request/XssHttpServletRequestWrapper.java new file mode 100644 index 0000000..cf93716 --- /dev/null +++ b/blade-core-boot/src/main/java/org/springblade/core/boot/request/XssHttpServletRequestWrapper.java @@ -0,0 +1,184 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.boot.request; + +import org.springblade.core.tool.utils.StringUtil; +import org.springblade.core.tool.utils.WebUtil; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; + +import jakarta.servlet.ReadListener; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * XSS过滤 + * + * @author Chill + */ +public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper { + + /** + * 没被包装过的HttpServletRequest(特殊场景,需要自己过滤) + */ + private final HttpServletRequest orgRequest; + /** + * 缓存报文,支持多次读取流 + */ + private byte[] body; + /** + * html过滤 + */ + private final static XssHtmlFilter HTML_FILTER = new XssHtmlFilter(); + + public XssHttpServletRequestWrapper(HttpServletRequest request) { + super(request); + orgRequest = request; + } + + @Override + public BufferedReader getReader() throws IOException { + return new BufferedReader(new InputStreamReader(getInputStream())); + } + + @Override + public ServletInputStream getInputStream() throws IOException { + if (super.getHeader(HttpHeaders.CONTENT_TYPE) == null) { + return super.getInputStream(); + } + + if (super.getHeader(HttpHeaders.CONTENT_TYPE).startsWith(MediaType.MULTIPART_FORM_DATA_VALUE)) { + return super.getInputStream(); + } + + if (body == null) { + body = xssEncode(WebUtil.getRequestBody(super.getInputStream())).getBytes(); + } + + final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body); + + return new ServletInputStream() { + + @Override + public int read() { + return byteArrayInputStream.read(); + } + + @Override + public boolean isFinished() { + return false; + } + + @Override + public boolean isReady() { + return false; + } + + @Override + public void setReadListener(ReadListener readListener) { + } + }; + } + + @Override + public String getParameter(String name) { + String value = super.getParameter(xssEncode(name)); + if (StringUtil.isNotBlank(value)) { + value = xssEncode(value); + } + return value; + } + + @Override + public String[] getParameterValues(String name) { + String[] parameters = super.getParameterValues(name); + if (parameters == null || parameters.length == 0) { + return null; + } + + for (int i = 0; i < parameters.length; i++) { + parameters[i] = xssEncode(parameters[i]); + } + return parameters; + } + + @Override + public Map getParameterMap() { + Map map = new LinkedHashMap<>(); + Map parameters = super.getParameterMap(); + for (String key : parameters.keySet()) { + String[] values = parameters.get(key); + for (int i = 0; i < values.length; i++) { + values[i] = xssEncode(values[i]); + } + map.put(key, values); + } + return map; + } + + @Override + public String getHeader(String name) { + String value = super.getHeader(xssEncode(name)); + if (StringUtil.isNotBlank(value)) { + value = xssEncode(value); + } + return value; + } + + private String xssEncode(String input) { + return HTML_FILTER.filter(input); + } + + /** + * 获取初始request + * + * @return HttpServletRequest + */ + public HttpServletRequest getOrgRequest() { + return orgRequest; + } + + /** + * 获取初始request + * + * @param request request + * @return HttpServletRequest + */ + public static HttpServletRequest getOrgRequest(HttpServletRequest request) { + if (request instanceof XssHttpServletRequestWrapper) { + return ((XssHttpServletRequestWrapper) request).getOrgRequest(); + } + return request; + } + +} diff --git a/blade-core-boot/src/main/java/org/springblade/core/boot/request/XssProperties.java b/blade-core-boot/src/main/java/org/springblade/core/boot/request/XssProperties.java new file mode 100644 index 0000000..1fc2081 --- /dev/null +++ b/blade-core-boot/src/main/java/org/springblade/core/boot/request/XssProperties.java @@ -0,0 +1,53 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.boot.request; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.ArrayList; +import java.util.List; + +/** + * Xss配置类 + * + * @author Chill + */ +@Data +@ConfigurationProperties("blade.xss") +public class XssProperties { + + /** + * 开启xss + */ + private Boolean enabled = true; + + /** + * 放行url + */ + private List skipUrl = new ArrayList<>(); + +} diff --git a/blade-core-boot/src/main/java/org/springblade/core/boot/resolver/TokenArgumentResolver.java b/blade-core-boot/src/main/java/org/springblade/core/boot/resolver/TokenArgumentResolver.java new file mode 100644 index 0000000..5b68e28 --- /dev/null +++ b/blade-core-boot/src/main/java/org/springblade/core/boot/resolver/TokenArgumentResolver.java @@ -0,0 +1,73 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.boot.resolver; + +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.secure.BladeUser; +import org.springblade.core.secure.utils.AuthUtil; +import org.springframework.core.MethodParameter; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +/** + * Token转化BladeUser + * + * @author Chill + */ +@Slf4j +public class TokenArgumentResolver implements HandlerMethodArgumentResolver { + + /** + * 入参筛选 + * + * @param methodParameter 参数集合 + * @return 格式化后的参数 + */ + @Override + public boolean supportsParameter(MethodParameter methodParameter) { + return methodParameter.getParameterType().equals(BladeUser.class); + } + + /** + * 出参设置 + * + * @param methodParameter 入参集合 + * @param modelAndViewContainer model 和 view + * @param nativeWebRequest web相关 + * @param webDataBinderFactory 入参解析 + * @return 包装对象 + */ + @Override + public Object resolveArgument(MethodParameter methodParameter, + ModelAndViewContainer modelAndViewContainer, + NativeWebRequest nativeWebRequest, + WebDataBinderFactory webDataBinderFactory) { + return AuthUtil.getUser(); + } + +} diff --git a/blade-core-boot/src/main/resources/banner.txt b/blade-core-boot/src/main/resources/banner.txt new file mode 100644 index 0000000..c0f1066 --- /dev/null +++ b/blade-core-boot/src/main/resources/banner.txt @@ -0,0 +1,8 @@ +${AnsiColor.BLUE} ______ _ _ ___ ___ +${AnsiColor.BLUE} | ___ \| | | | \ \ / / +${AnsiColor.BLUE} | |_/ /| | __ _ __| | ___ \ V / +${AnsiColor.BLUE} | ___ \| | / _` | / _` | / _ \ > < +${AnsiColor.BLUE} | |_/ /| || (_| || (_| || __/ / . \ +${AnsiColor.BLUE} \____/ |_| \__,_| \__,_| \___|/__/ \__\ + +${AnsiColor.BLUE}:: BladeX ${blade.service.version} :: ${spring.application.name}:${AnsiColor.RED}${blade.env}${AnsiColor.BLUE} :: Running SpringBoot ${spring-boot.version} :: ${AnsiColor.BRIGHT_BLACK} diff --git a/blade-core-boot/src/main/resources/blade-boot.yml b/blade-core-boot/src/main/resources/blade-boot.yml new file mode 100644 index 0000000..6a07495 --- /dev/null +++ b/blade-core-boot/src/main/resources/blade-boot.yml @@ -0,0 +1,35 @@ +#服务器配置 +server: + undertow: + # 线程配置 + threads: + # 设置IO线程数, 它主要执行非阻塞的任务,它们会负责多个连接, 默认设置每个CPU核心一个线程 + io: 16 + # 阻塞任务线程池, 当执行类似servlet请求阻塞操作, undertow会从这个线程池中取得线程,它的值设置取决于系统的负载 + worker: 400 + # 以下的配置会影响buffer,这些buffer会用于服务器连接的IO操作,有点类似netty的池化内存管理 + buffer-size: 1024 + # 是否分配的直接内存 + direct-buffers: true + servlet: + # 编码配置 + encoding: + charset: UTF-8 + force: true + +#spring配置 +spring: + servlet: + multipart: + enabled: true + max-file-size: 1024MB + max-request-size: 1024MB + mvc: + throw-exception-if-no-handler-found: true + web: + resources: + add-mappings: false + devtools: + restart: + log-condition-evaluation-delta: false + diff --git a/blade-core-boot/src/main/resources/static/favicon.ico b/blade-core-boot/src/main/resources/static/favicon.ico new file mode 100644 index 0000000..2d915c8 Binary files /dev/null and b/blade-core-boot/src/main/resources/static/favicon.ico differ diff --git a/blade-core-cloud/pom.xml b/blade-core-cloud/pom.xml new file mode 100644 index 0000000..e1ec79e --- /dev/null +++ b/blade-core-cloud/pom.xml @@ -0,0 +1,54 @@ + + + 4.0.0 + + BladeX-Tool + org.springblade + ${revision} + + + blade-core-cloud + ${project.artifactId} + ${project.parent.version} + jar + + + + + + org.springblade + blade-core-launch + + + org.springblade + blade-core-context + + + org.springblade + blade-starter-auth + + + org.springblade + blade-starter-loadbalancer + + + + de.codecentric + spring-boot-admin-starter-client + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-sentinel + + + + org.springblade + blade-core-auto + provided + + + + diff --git a/blade-core-cloud/src/main/java/org/springblade/core/cloud/annotation/ApiVersion.java b/blade-core-cloud/src/main/java/org/springblade/core/cloud/annotation/ApiVersion.java new file mode 100644 index 0000000..b8818f7 --- /dev/null +++ b/blade-core-cloud/src/main/java/org/springblade/core/cloud/annotation/ApiVersion.java @@ -0,0 +1,48 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.cloud.annotation; + +import java.lang.annotation.*; + +/** + * header 版本 处理 + * + * @author L.cm + */ +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface ApiVersion { + + /** + * header 路径中的版本 + * + * @return 版本号 + */ + String value() default ""; + +} diff --git a/blade-core-cloud/src/main/java/org/springblade/core/cloud/annotation/UrlVersion.java b/blade-core-cloud/src/main/java/org/springblade/core/cloud/annotation/UrlVersion.java new file mode 100644 index 0000000..d2efb58 --- /dev/null +++ b/blade-core-cloud/src/main/java/org/springblade/core/cloud/annotation/UrlVersion.java @@ -0,0 +1,47 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.cloud.annotation; + +import java.lang.annotation.*; + +/** + * 注解用于生成 requestMappingInfo 时候直接拼接路径规则,自动放置于方法路径开始部分 + * + * @author L.cm + */ +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface UrlVersion { + + /** + * url 路径中的版本 + * + * @return 版本号 + */ + String value() default ""; +} diff --git a/blade-core-cloud/src/main/java/org/springblade/core/cloud/annotation/VersionMapping.java b/blade-core-cloud/src/main/java/org/springblade/core/cloud/annotation/VersionMapping.java new file mode 100644 index 0000000..61078b9 --- /dev/null +++ b/blade-core-cloud/src/main/java/org/springblade/core/cloud/annotation/VersionMapping.java @@ -0,0 +1,116 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.cloud.annotation; + +import org.springframework.core.annotation.AliasFor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.RequestMapping; + +import java.lang.annotation.*; + +/** + * 版本号处理 + * + *

+ * 1. url 版本号:添加到 url 前 + * 2. Accept 版本:application/vnd.blade.VERSION+json + *

+ * + * @author L.cm + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@RequestMapping +@UrlVersion +@ApiVersion +@Validated +public @interface VersionMapping { + /** + * Alias for {@link RequestMapping#name}. + * @return {String[]} + */ + @AliasFor(annotation = RequestMapping.class) + String name() default ""; + + /** + * Alias for {@link RequestMapping#value}. + * @return {String[]} + */ + @AliasFor(annotation = RequestMapping.class) + String[] value() default {}; + + /** + * Alias for {@link RequestMapping#path}. + * @return {String[]} + */ + @AliasFor(annotation = RequestMapping.class) + String[] path() default {}; + + /** + * Alias for {@link RequestMapping#params}. + * @return {String[]} + */ + @AliasFor(annotation = RequestMapping.class) + String[] params() default {}; + + /** + * Alias for {@link RequestMapping#headers}. + * @return {String[]} + */ + @AliasFor(annotation = RequestMapping.class) + String[] headers() default {}; + + /** + * Alias for {@link RequestMapping#consumes}. + * @return {String[]} + */ + @AliasFor(annotation = RequestMapping.class) + String[] consumes() default {}; + + /** + * Alias for {@link RequestMapping#produces}. + * default json utf-8 + * @return {String[]} + */ + @AliasFor(annotation = RequestMapping.class) + String[] produces() default {}; + + /** + * Alias for {@link UrlVersion#value}. + * @return {String} + */ + @AliasFor(annotation = UrlVersion.class, attribute = "value") + String urlVersion() default ""; + + /** + * Alias for {@link ApiVersion#value}. + * @return {String} + */ + @AliasFor(annotation = ApiVersion.class, attribute = "value") + String apiVersion() default ""; + +} diff --git a/blade-core-cloud/src/main/java/org/springblade/core/cloud/client/BladeCloudApplication.java b/blade-core-cloud/src/main/java/org/springblade/core/cloud/client/BladeCloudApplication.java new file mode 100644 index 0000000..9d6a861 --- /dev/null +++ b/blade-core-cloud/src/main/java/org/springblade/core/cloud/client/BladeCloudApplication.java @@ -0,0 +1,49 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.cloud.client; + +import org.springblade.core.launch.constant.AppConstant; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; +import org.springframework.cloud.openfeign.EnableFeignClients; + +import java.lang.annotation.*; + +/** + * Cloud启动注解配置 + * + * @author Chill + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@EnableDiscoveryClient +@EnableFeignClients(AppConstant.BASE_PACKAGES) +@SpringBootApplication +public @interface BladeCloudApplication { + +} diff --git a/blade-core-cloud/src/main/java/org/springblade/core/cloud/feign/BladeFallbackFactory.java b/blade-core-cloud/src/main/java/org/springblade/core/cloud/feign/BladeFallbackFactory.java new file mode 100644 index 0000000..8313a29 --- /dev/null +++ b/blade-core-cloud/src/main/java/org/springblade/core/cloud/feign/BladeFallbackFactory.java @@ -0,0 +1,54 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.cloud.feign; + +import feign.Target; +import org.springframework.cloud.openfeign.FallbackFactory; +import lombok.AllArgsConstructor; +import org.springframework.cglib.proxy.Enhancer; + +/** + * 默认 Fallback,避免写过多fallback类 + * + * @param 泛型标记 + * @author L.cm + */ +@AllArgsConstructor +public class BladeFallbackFactory implements FallbackFactory { + private final Target target; + + @Override + @SuppressWarnings("unchecked") + public T create(Throwable cause) { + final Class targetType = target.type(); + final String targetName = target.name(); + Enhancer enhancer = new Enhancer(); + enhancer.setSuperclass(targetType); + enhancer.setUseCache(true); + enhancer.setCallback(new BladeFeignFallback<>(targetType, targetName, cause)); + return (T) enhancer.create(); + } +} diff --git a/blade-core-cloud/src/main/java/org/springblade/core/cloud/feign/BladeFeignFallback.java b/blade-core-cloud/src/main/java/org/springblade/core/cloud/feign/BladeFeignFallback.java new file mode 100644 index 0000000..5e21990 --- /dev/null +++ b/blade-core-cloud/src/main/java/org/springblade/core/cloud/feign/BladeFeignFallback.java @@ -0,0 +1,111 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.cloud.feign; + +import com.fasterxml.jackson.databind.JsonNode; +import feign.FeignException; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.tool.api.R; +import org.springblade.core.tool.api.ResultCode; +import org.springblade.core.tool.jackson.JsonUtil; +import org.springblade.core.tool.utils.ObjectUtil; +import org.springframework.cglib.proxy.MethodInterceptor; +import org.springframework.cglib.proxy.MethodProxy; +import org.springframework.lang.Nullable; + +import java.lang.reflect.Method; +import java.util.*; + +/** + * blade fallBack 代理处理 + * + * @author L.cm + */ +@Slf4j +@AllArgsConstructor +public class BladeFeignFallback implements MethodInterceptor { + private final Class targetType; + private final String targetName; + private final Throwable cause; + private final static String CODE = "code"; + + @Nullable + @Override + public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { + String errorMessage = cause.getMessage(); + log.error("BladeFeignFallback:[{}.{}] serviceId:[{}] message:[{}]", targetType.getName(), method.getName(), targetName, errorMessage); + Class returnType = method.getReturnType(); + // 集合类型反馈空集合 + if (List.class == returnType || Collection.class == returnType) { + return Collections.emptyList(); + } + if (Set.class == returnType) { + return Collections.emptySet(); + } + if (Map.class == returnType) { + return Collections.emptyMap(); + } + // 暂时不支持 flux,rx,异步等,返回值不是 R,直接返回 null。 + if (R.class != returnType) { + return null; + } + // 非 FeignException + if (!(cause instanceof FeignException)) { + return R.fail(ResultCode.INTERNAL_SERVER_ERROR, errorMessage); + } + FeignException exception = (FeignException) cause; + byte[] content = exception.content(); + // 如果返回的数据为空 + if (ObjectUtil.isEmpty(content)) { + return R.fail(ResultCode.INTERNAL_SERVER_ERROR, errorMessage); + } + // 转换成 jsonNode 读取,因为直接转换,可能 对方放回的并 不是 R 的格式。 + JsonNode resultNode = JsonUtil.readTree(content); + // 判断是否 R 格式 返回体 + if (resultNode.has(CODE)) { + return JsonUtil.getInstance().convertValue(resultNode, R.class); + } + return R.fail(resultNode.toString()); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + BladeFeignFallback that = (BladeFeignFallback) o; + return targetType.equals(that.targetType); + } + + @Override + public int hashCode() { + return Objects.hash(targetType); + } +} diff --git a/blade-core-cloud/src/main/java/org/springblade/core/cloud/feign/BladeFeignRequestInterceptor.java b/blade-core-cloud/src/main/java/org/springblade/core/cloud/feign/BladeFeignRequestInterceptor.java new file mode 100644 index 0000000..fe1dda1 --- /dev/null +++ b/blade-core-cloud/src/main/java/org/springblade/core/cloud/feign/BladeFeignRequestInterceptor.java @@ -0,0 +1,57 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.cloud.feign; + +import feign.RequestInterceptor; +import feign.RequestTemplate; +import org.springblade.core.tool.constant.BladeConstant; +import org.springblade.core.tool.utils.ThreadLocalUtil; +import org.springframework.http.HttpHeaders; + +/** + * feign 传递Request header + * + *

+ * https://blog.csdn.net/u014519194/article/details/77160958 + * http://tietang.wang/2016/02/25/hystrix/Hystrix%E5%8F%82%E6%95%B0%E8%AF%A6%E8%A7%A3/ + * https://github.com/Netflix/Hystrix/issues/92#issuecomment-260548068 + *

+ * + * @author L.cm + */ +public class BladeFeignRequestInterceptor implements RequestInterceptor { + + @Override + public void apply(RequestTemplate requestTemplate) { + HttpHeaders headers = ThreadLocalUtil.get(BladeConstant.CONTEXT_KEY); + if (headers != null && !headers.isEmpty()) { + headers.forEach((key, values) -> + values.forEach(value -> requestTemplate.header(key, value)) + ); + } + } + +} diff --git a/blade-core-cloud/src/main/java/org/springblade/core/cloud/feign/EnableBladeFeign.java b/blade-core-cloud/src/main/java/org/springblade/core/cloud/feign/EnableBladeFeign.java new file mode 100644 index 0000000..990a6e9 --- /dev/null +++ b/blade-core-cloud/src/main/java/org/springblade/core/cloud/feign/EnableBladeFeign.java @@ -0,0 +1,90 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ + +package org.springblade.core.cloud.feign; + + +import org.springblade.core.launch.constant.AppConstant; +import org.springframework.cloud.openfeign.EnableFeignClients; + +import java.lang.annotation.*; + +/** + * 开启Feign注解 + * + * @author Chill + */ +@Documented +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@EnableFeignClients(AppConstant.BASE_PACKAGES) +public @interface EnableBladeFeign { + /** + * Alias for the {@link #basePackages()} attribute. Allows for more concise annotation + * declarations e.g.: {@code @ComponentScan("org.my.pkg")} instead of + * {@code @ComponentScan(basePackages="org.my.pkg")}. + * + * @return the array of 'basePackages'. + */ + String[] value() default {}; + + /** + * Base packages to scan for annotated components. + *

+ * {@link #value()} is an alias for (and mutually exclusive with) this attribute. + *

+ * Use {@link #basePackageClasses()} for a type-safe alternative to String-based + * package names. + * + * @return the array of 'basePackages'. + */ + String[] basePackages() default {}; + + /** + * Type-safe alternative to {@link #basePackages()} for specifying the packages to + * scan for annotated components. The package of each class specified will be scanned. + *

+ * Consider creating a special no-op marker class or interface in each package that + * serves no purpose other than being referenced by this attribute. + * + * @return the array of 'basePackageClasses'. + */ + Class[] basePackageClasses() default {}; + + /** + * A custom @Configuration for all feign clients. Can contain override + * @Bean definition for the pieces that make up the client, for instance + * {@link feign.codec.Decoder}, {@link feign.codec.Encoder}, {@link feign.Contract}. + */ + Class[] defaultConfiguration() default {}; + + /** + * List of classes annotated with @FeignClient. If not empty, disables classpath scanning. + * + * @return + */ + Class[] clients() default {}; +} diff --git a/blade-core-cloud/src/main/java/org/springblade/core/cloud/http/BladeHttpConfiguration.java b/blade-core-cloud/src/main/java/org/springblade/core/cloud/http/BladeHttpConfiguration.java new file mode 100644 index 0000000..beeb230 --- /dev/null +++ b/blade-core-cloud/src/main/java/org/springblade/core/cloud/http/BladeHttpConfiguration.java @@ -0,0 +1,39 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.cloud.http; + +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; + +/** + * http 配置 + * + * @author L.cm + */ +@AutoConfiguration +@EnableConfigurationProperties(BladeHttpProperties.class) +public class BladeHttpConfiguration { +} diff --git a/blade-core-cloud/src/main/java/org/springblade/core/cloud/http/BladeHttpProperties.java b/blade-core-cloud/src/main/java/org/springblade/core/cloud/http/BladeHttpProperties.java new file mode 100644 index 0000000..4eb5e7b --- /dev/null +++ b/blade-core-cloud/src/main/java/org/springblade/core/cloud/http/BladeHttpProperties.java @@ -0,0 +1,74 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.cloud.http; + +import lombok.Getter; +import lombok.Setter; +import org.springblade.core.launch.log.BladeLogLevel; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.cloud.context.config.annotation.RefreshScope; + +import java.util.concurrent.TimeUnit; + +/** + * http 配置 + * + * @author L.cm + */ +@Getter +@Setter +@RefreshScope +@ConfigurationProperties("blade.http") +public class BladeHttpProperties { + /** + * 最大连接数,默认:200 + */ + private int maxConnections = 200; + /** + * 连接存活时间,默认:900L + */ + private long timeToLive = 900L; + /** + * 连接池存活时间单位,默认:秒 + */ + private TimeUnit timeUnit = TimeUnit.SECONDS; + /** + * 链接超时,默认:2000毫秒 + */ + private int connectionTimeout = 2000; + /** + * 是否支持重定向,默认:true + */ + private boolean followRedirects = true; + /** + * 关闭证书校验 + */ + private boolean disableSslValidation = true; + /** + * 日志级别 + */ + private BladeLogLevel level = BladeLogLevel.NONE; +} diff --git a/blade-core-cloud/src/main/java/org/springblade/core/cloud/http/LbRestTemplate.java b/blade-core-cloud/src/main/java/org/springblade/core/cloud/http/LbRestTemplate.java new file mode 100644 index 0000000..b4ac109 --- /dev/null +++ b/blade-core-cloud/src/main/java/org/springblade/core/cloud/http/LbRestTemplate.java @@ -0,0 +1,27 @@ +package org.springblade.core.cloud.http; + +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.web.client.RestTemplate; + +import java.util.List; + +/** + * Loadbalancer RestTemplate + * + * @author L.cm + */ +public class LbRestTemplate extends RestTemplate { + + public LbRestTemplate() { + super(); + } + + public LbRestTemplate(ClientHttpRequestFactory requestFactory) { + super(requestFactory); + } + + public LbRestTemplate(List> messageConverters) { + super(messageConverters); + } +} diff --git a/blade-core-cloud/src/main/java/org/springblade/core/cloud/http/RestTemplateConfiguration.java b/blade-core-cloud/src/main/java/org/springblade/core/cloud/http/RestTemplateConfiguration.java new file mode 100644 index 0000000..aa6b1e0 --- /dev/null +++ b/blade-core-cloud/src/main/java/org/springblade/core/cloud/http/RestTemplateConfiguration.java @@ -0,0 +1,200 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.cloud.http; + + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import okhttp3.ConnectionPool; +import okhttp3.OkHttpClient; +import org.springblade.core.cloud.http.client.OkHttp3ClientHttpRequestFactory; +import org.springblade.core.cloud.http.logger.HttpLoggingInterceptor; +import org.springblade.core.cloud.http.logger.OkHttpSlf4jLogger; +import org.springblade.core.tool.ssl.DisableValidationTrustManager; +import org.springblade.core.tool.ssl.TrustAllHostNames; +import org.springblade.core.tool.utils.Charsets; +import org.springblade.core.tool.utils.Holder; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.cloud.client.loadbalancer.LoadBalanced; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.StringHttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.web.client.RestTemplate; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * Http RestTemplateHeaderInterceptor 配置 + * + * @author L.cm + */ +@Slf4j +@RequiredArgsConstructor +@AutoConfiguration +@ConditionalOnClass(OkHttpClient.class) +@ConditionalOnProperty(value = "blade.http.enabled", matchIfMissing = true) +public class RestTemplateConfiguration { + private final BladeHttpProperties properties; + + /** + * okhttp3 请求日志拦截器 + * + * @return HttpLoggingInterceptor + */ + @Bean + public HttpLoggingInterceptor loggingInterceptor() { + HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor(new OkHttpSlf4jLogger()); + interceptor.setLevel(properties.getLevel()); + return interceptor; + } + + /** + * okhttp3 链接池配置 + * + * @return okhttp3.ConnectionPool + */ + @Bean + @ConditionalOnMissingBean + public ConnectionPool httpClientConnectionPool() { + int maxTotalConnections = properties.getMaxConnections(); + long timeToLive = properties.getTimeToLive(); + TimeUnit ttlUnit = properties.getTimeUnit(); + return new ConnectionPool(maxTotalConnections, timeToLive, ttlUnit); + } + + /** + * 配置OkHttpClient + * + * @param connectionPool 链接池配置 + * @param interceptor 拦截器 + * @return OkHttpClient + */ + @Bean + @ConditionalOnMissingBean + public OkHttpClient okHttpClient(ConnectionPool connectionPool, HttpLoggingInterceptor interceptor) { + boolean followRedirects = properties.isFollowRedirects(); + int connectTimeout = properties.getConnectionTimeout(); + return this.createBuilder(properties.isDisableSslValidation()) + .connectTimeout(connectTimeout, TimeUnit.MILLISECONDS) + .writeTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .followRedirects(followRedirects) + .connectionPool(connectionPool) + .addInterceptor(interceptor) + .build(); + } + + private OkHttpClient.Builder createBuilder(boolean disableSslValidation) { + OkHttpClient.Builder builder = new OkHttpClient.Builder(); + if (disableSslValidation) { + try { + X509TrustManager disabledTrustManager = DisableValidationTrustManager.INSTANCE; + TrustManager[] trustManagers = new TrustManager[]{disabledTrustManager}; + SSLContext sslContext = SSLContext.getInstance("SSL"); + sslContext.init(null, trustManagers, Holder.SECURE_RANDOM); + SSLSocketFactory disabledSslSocketFactory = sslContext.getSocketFactory(); + builder.sslSocketFactory(disabledSslSocketFactory, disabledTrustManager); + builder.hostnameVerifier(TrustAllHostNames.INSTANCE); + } catch (NoSuchAlgorithmException | KeyManagementException e) { + log.warn("Error setting SSLSocketFactory in OKHttpClient", e); + } + } + return builder; + } + + @Bean + public RestTemplateHeaderInterceptor requestHeaderInterceptor() { + return new RestTemplateHeaderInterceptor(); + } + + @AutoConfiguration + @RequiredArgsConstructor + @ConditionalOnClass(OkHttpClient.class) + @ConditionalOnProperty(value = "blade.http.rest-template.enable") + public static class RestTemplateAutoConfiguration { + private final ApplicationContext context; + + /** + * 普通的 RestTemplate,不透传请求头,一般只做外部 http 调用 + * + * @param okHttpClient OkHttpClient + * @return RestTemplate + */ + @Bean + @ConditionalOnMissingBean + public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder, OkHttpClient okHttpClient) { + restTemplateBuilder.requestFactory(() -> new OkHttp3ClientHttpRequestFactory(okHttpClient)); + RestTemplate restTemplate = restTemplateBuilder.build(); + configMessageConverters(context, restTemplate.getMessageConverters()); + return restTemplate; + } + } + + @AutoConfiguration + @RequiredArgsConstructor + @ConditionalOnClass(OkHttpClient.class) + @ConditionalOnProperty(value = "blade.http.lb-rest-template.enable") + public static class LbRestTemplateAutoConfiguration { + private final ApplicationContext context; + + /** + * 支持负载均衡的 LbRestTemplate + * + * @param okHttpClient OkHttpClient + * @return LbRestTemplate + */ + @Bean + @LoadBalanced + @ConditionalOnMissingBean + public LbRestTemplate lbRestTemplate(RestTemplateBuilder restTemplateBuilder, OkHttpClient okHttpClient) { + restTemplateBuilder.requestFactory(() -> new OkHttp3ClientHttpRequestFactory(okHttpClient)); + LbRestTemplate restTemplate = restTemplateBuilder.build(LbRestTemplate.class); + restTemplate.getInterceptors().add(context.getBean(RestTemplateHeaderInterceptor.class)); + configMessageConverters(context, restTemplate.getMessageConverters()); + return restTemplate; + } + } + + private static void configMessageConverters(ApplicationContext context, List> converters) { + converters.removeIf(x -> x instanceof StringHttpMessageConverter || x instanceof MappingJackson2HttpMessageConverter); + converters.add(new StringHttpMessageConverter(Charsets.UTF_8)); + converters.add(new MappingJackson2HttpMessageConverter(context.getBean(ObjectMapper.class))); + } +} diff --git a/blade-core-cloud/src/main/java/org/springblade/core/cloud/http/RestTemplateHeaderInterceptor.java b/blade-core-cloud/src/main/java/org/springblade/core/cloud/http/RestTemplateHeaderInterceptor.java new file mode 100644 index 0000000..a3b2e32 --- /dev/null +++ b/blade-core-cloud/src/main/java/org/springblade/core/cloud/http/RestTemplateHeaderInterceptor.java @@ -0,0 +1,57 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.cloud.http; + +import lombok.AllArgsConstructor; +import org.springblade.core.tool.constant.BladeConstant; +import org.springblade.core.tool.utils.ThreadLocalUtil; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpRequest; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.lang.NonNull; + +import java.io.IOException; + +/** + * RestTemplateHeaderInterceptor 传递Request header + * + * @author L.cm + */ +@AllArgsConstructor +public class RestTemplateHeaderInterceptor implements ClientHttpRequestInterceptor { + @NonNull + @Override + public ClientHttpResponse intercept(@NonNull HttpRequest request, @NonNull byte[] bytes, @NonNull ClientHttpRequestExecution execution) throws IOException { + HttpHeaders headers = ThreadLocalUtil.get(BladeConstant.CONTEXT_KEY); + if (headers != null && !headers.isEmpty()) { + HttpHeaders httpHeaders = request.getHeaders(); + headers.forEach((key, values) -> values.forEach(value -> httpHeaders.add(key, value))); + } + return execution.execute(request, bytes); + } +} diff --git a/blade-core-cloud/src/main/java/org/springblade/core/cloud/http/client/AbstractStreamingClientHttpRequest.java b/blade-core-cloud/src/main/java/org/springblade/core/cloud/http/client/AbstractStreamingClientHttpRequest.java new file mode 100644 index 0000000..3b362c6 --- /dev/null +++ b/blade-core-cloud/src/main/java/org/springblade/core/cloud/http/client/AbstractStreamingClientHttpRequest.java @@ -0,0 +1,86 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.cloud.http.client; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.StreamingHttpOutputMessage; +import org.springframework.http.client.AbstractClientHttpRequest; +import org.springframework.http.client.ClientHttpRequest; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.FastByteArrayOutputStream; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * Abstract base for {@link ClientHttpRequest} that also implement + * {@link StreamingHttpOutputMessage}. Ensures that headers and + * body are not written multiple times. + * + * @author Arjen Poutsma + * @since 6.1 + */ +public abstract class AbstractStreamingClientHttpRequest extends AbstractClientHttpRequest + implements StreamingHttpOutputMessage { + + @Nullable + private Body body; + + @Nullable + private FastByteArrayOutputStream bodyStream; + + + @Override + protected final OutputStream getBodyInternal(HttpHeaders headers) { + Assert.state(this.body == null, "Invoke either getBody or setBody; not both"); + + if (this.bodyStream == null) { + this.bodyStream = new FastByteArrayOutputStream(1024); + } + return this.bodyStream; + } + + @Override + public final void setBody(Body body) { + Assert.notNull(body, "Body must not be null"); + assertNotExecuted(); + Assert.state(this.bodyStream == null, "Invoke either getBody or setBody; not both"); + + this.body = body; + } + + @Override + protected final ClientHttpResponse executeInternal(HttpHeaders headers) throws IOException { + if (this.body == null && this.bodyStream != null) { + this.body = outputStream -> this.bodyStream.writeTo(outputStream); + } + return executeInternal(headers, this.body); + } + + + /** + * Abstract template method that writes the given headers and content to the HTTP request. + * + * @param headers the HTTP headers + * @param body the HTTP body, may be {@code null} if no body was {@linkplain #setBody(Body) set} + * @return the response object for the executed request + * @since 6.1 + */ + protected abstract ClientHttpResponse executeInternal(HttpHeaders headers, @Nullable Body body) throws IOException; + +} diff --git a/blade-core-cloud/src/main/java/org/springblade/core/cloud/http/client/OkHttp3ClientHttpRequest.java b/blade-core-cloud/src/main/java/org/springblade/core/cloud/http/client/OkHttp3ClientHttpRequest.java new file mode 100644 index 0000000..3b682a2 --- /dev/null +++ b/blade-core-cloud/src/main/java/org/springblade/core/cloud/http/client/OkHttp3ClientHttpRequest.java @@ -0,0 +1,137 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springblade.core.cloud.http.client; + +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okio.BufferedSink; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.client.ClientHttpRequest; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +import java.io.IOException; +import java.net.URI; + +/** + * {@link ClientHttpRequest} implementation based on OkHttp 3.x. + * + *

Created via the {@link OkHttp3ClientHttpRequestFactory}. + * + * @author Luciano Leggieri + * @author Arjen Poutsma + * @author Roy Clarkson + * @since 4.3 + */ +public class OkHttp3ClientHttpRequest extends AbstractStreamingClientHttpRequest { + + private final OkHttpClient client; + + private final URI uri; + + private final HttpMethod method; + + + public OkHttp3ClientHttpRequest(OkHttpClient client, URI uri, HttpMethod method) { + this.client = client; + this.uri = uri; + this.method = method; + } + + + @Override + public HttpMethod getMethod() { + return this.method; + } + + @Override + public URI getURI() { + return this.uri; + } + + @Override + @SuppressWarnings("removal") + protected ClientHttpResponse executeInternal(HttpHeaders headers, @Nullable Body body) throws IOException { + + RequestBody requestBody; + if (body != null) { + requestBody = new BodyRequestBody(headers, body); + } else if (okhttp3.internal.http.HttpMethod.requiresRequestBody(getMethod().name())) { + String header = headers.getFirst(HttpHeaders.CONTENT_TYPE); + MediaType contentType = (header != null) ? MediaType.parse(header) : null; + requestBody = RequestBody.create(contentType, new byte[0]); + } else { + requestBody = null; + } + Request.Builder builder = new Request.Builder() + .url(this.uri.toURL()); + builder.method(this.method.name(), requestBody); + headers.forEach((headerName, headerValues) -> { + for (String headerValue : headerValues) { + builder.addHeader(headerName, headerValue); + } + }); + Request request = builder.build(); + return new OkHttp3ClientHttpResponse(this.client.newCall(request).execute()); + } + + + private static class BodyRequestBody extends RequestBody { + + private final HttpHeaders headers; + + private final Body body; + + + public BodyRequestBody(HttpHeaders headers, Body body) { + this.headers = headers; + this.body = body; + } + + @Override + public long contentLength() { + return this.headers.getContentLength(); + } + + @Nullable + @Override + public MediaType contentType() { + String contentType = this.headers.getFirst(HttpHeaders.CONTENT_TYPE); + if (StringUtils.hasText(contentType)) { + return MediaType.parse(contentType); + } else { + return null; + } + } + + @Override + public void writeTo(BufferedSink sink) throws IOException { + this.body.writeTo(sink.outputStream()); + } + + @Override + public boolean isOneShot() { + return !this.body.repeatable(); + } + } + + +} diff --git a/blade-core-cloud/src/main/java/org/springblade/core/cloud/http/client/OkHttp3ClientHttpRequestFactory.java b/blade-core-cloud/src/main/java/org/springblade/core/cloud/http/client/OkHttp3ClientHttpRequestFactory.java new file mode 100644 index 0000000..ac162fb --- /dev/null +++ b/blade-core-cloud/src/main/java/org/springblade/core/cloud/http/client/OkHttp3ClientHttpRequestFactory.java @@ -0,0 +1,156 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springblade.core.cloud.http.client; + +import okhttp3.Cache; +import okhttp3.OkHttpClient; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.http.HttpMethod; +import org.springframework.http.client.ClientHttpRequest; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.util.Assert; + +import java.io.IOException; +import java.net.URI; +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +/** + * {@link ClientHttpRequestFactory} implementation that uses + * OkHttp 3.x to create requests. + * + * @author Luciano Leggieri + * @author Arjen Poutsma + * @author Roy Clarkson + * @since 4.3 + */ +public class OkHttp3ClientHttpRequestFactory implements ClientHttpRequestFactory, DisposableBean { + + private OkHttpClient client; + + private final boolean defaultClient; + + + /** + * Create a factory with a default {@link OkHttpClient} instance. + */ + public OkHttp3ClientHttpRequestFactory() { + this.client = new OkHttpClient(); + this.defaultClient = true; + } + + /** + * Create a factory with the given {@link OkHttpClient} instance. + * + * @param client the client to use + */ + public OkHttp3ClientHttpRequestFactory(OkHttpClient client) { + Assert.notNull(client, "OkHttpClient must not be null"); + this.client = client; + this.defaultClient = false; + } + + + /** + * Set the underlying read timeout in milliseconds. + * A value of 0 specifies an infinite timeout. + */ + public void setReadTimeout(int readTimeout) { + this.client = this.client.newBuilder() + .readTimeout(readTimeout, TimeUnit.MILLISECONDS) + .build(); + } + + /** + * Set the underlying read timeout in milliseconds. + * A value of 0 specifies an infinite timeout. + * + * @since 6.1 + */ + public void setReadTimeout(Duration readTimeout) { + this.client = this.client.newBuilder() + .readTimeout(readTimeout) + .build(); + } + + /** + * Set the underlying write timeout in milliseconds. + * A value of 0 specifies an infinite timeout. + */ + public void setWriteTimeout(int writeTimeout) { + this.client = this.client.newBuilder() + .writeTimeout(writeTimeout, TimeUnit.MILLISECONDS) + .build(); + } + + /** + * Set the underlying write timeout in milliseconds. + * A value of 0 specifies an infinite timeout. + * + * @since 6.1 + */ + public void setWriteTimeout(Duration writeTimeout) { + this.client = this.client.newBuilder() + .writeTimeout(writeTimeout) + .build(); + } + + /** + * Set the underlying connect timeout in milliseconds. + * A value of 0 specifies an infinite timeout. + */ + public void setConnectTimeout(int connectTimeout) { + this.client = this.client.newBuilder() + .connectTimeout(connectTimeout, TimeUnit.MILLISECONDS) + .build(); + } + + /** + * Set the underlying connect timeout in milliseconds. + * A value of 0 specifies an infinite timeout. + * + * @since 6.1 + */ + public void setConnectTimeout(Duration connectTimeout) { + this.client = this.client.newBuilder() + .connectTimeout(connectTimeout) + .build(); + } + + + @NotNull + @Override + public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) { + return new OkHttp3ClientHttpRequest(this.client, uri, httpMethod); + } + + + @Override + public void destroy() throws IOException { + if (this.defaultClient) { + // Clean up the client if we created it in the constructor + Cache cache = this.client.cache(); + if (cache != null) { + cache.close(); + } + this.client.dispatcher().executorService().shutdown(); + this.client.connectionPool().evictAll(); + } + } + +} diff --git a/blade-core-cloud/src/main/java/org/springblade/core/cloud/http/client/OkHttp3ClientHttpResponse.java b/blade-core-cloud/src/main/java/org/springblade/core/cloud/http/client/OkHttp3ClientHttpResponse.java new file mode 100644 index 0000000..297cd35 --- /dev/null +++ b/blade-core-cloud/src/main/java/org/springblade/core/cloud/http/client/OkHttp3ClientHttpResponse.java @@ -0,0 +1,90 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.cloud.http.client; + +import okhttp3.Response; +import okhttp3.ResponseBody; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +import java.io.IOException; +import java.io.InputStream; + +/** + * {@link ClientHttpResponse} implementation based on OkHttp 3.x. + * + * @author Luciano Leggieri + * @author Arjen Poutsma + * @author Roy Clarkson + * @since 4.3 + */ +public class OkHttp3ClientHttpResponse implements ClientHttpResponse { + + private final Response response; + + @Nullable + private volatile HttpHeaders headers; + + + public OkHttp3ClientHttpResponse(Response response) { + Assert.notNull(response, "Response must not be null"); + this.response = response; + } + + + @Override + public HttpStatusCode getStatusCode() throws IOException { + return HttpStatusCode.valueOf(this.response.code()); + } + + @Override + public String getStatusText() { + return this.response.message(); + } + + @Override + public InputStream getBody() throws IOException { + ResponseBody body = this.response.body(); + return (body != null ? body.byteStream() : InputStream.nullInputStream()); + } + + @Override + public HttpHeaders getHeaders() { + HttpHeaders headers = this.headers; + if (headers == null) { + headers = new HttpHeaders(); + for (String headerName : this.response.headers().names()) { + for (String headerValue : this.response.headers(headerName)) { + headers.add(headerName, headerValue); + } + } + this.headers = headers; + } + return headers; + } + + @Override + public void close() { + ResponseBody body = this.response.body(); + if (body != null) { + body.close(); + } + } + +} diff --git a/blade-core-cloud/src/main/java/org/springblade/core/cloud/http/logger/HttpLoggingInterceptor.java b/blade-core-cloud/src/main/java/org/springblade/core/cloud/http/logger/HttpLoggingInterceptor.java new file mode 100644 index 0000000..c4920a5 --- /dev/null +++ b/blade-core-cloud/src/main/java/org/springblade/core/cloud/http/logger/HttpLoggingInterceptor.java @@ -0,0 +1,266 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.cloud.http.logger; + +import okhttp3.*; +import okhttp3.internal.http.HttpHeaders; +import okio.Buffer; +import okio.BufferedSource; +import okio.GzipSource; +import org.springblade.core.launch.log.BladeLogLevel; + +import java.io.EOFException; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +/** + * An OkHttp interceptor which logs request and response information. Can be applied as an + * {@linkplain OkHttpClient#interceptors() application interceptor} or as a {@linkplain + * OkHttpClient#networkInterceptors() network interceptor}.

The format of the logs created by + * this class should not be considered stable and may change slightly between releases. If you need + * a stable logging format, use your own interceptor. + * + * @author L.cm + */ +public final class HttpLoggingInterceptor implements Interceptor { + private static final Charset UTF8 = StandardCharsets.UTF_8; + private final Logger logger; + private volatile BladeLogLevel level = BladeLogLevel.NONE; + + public interface Logger { + /** + * log + * @param message message + */ + void log(String message); + } + + public HttpLoggingInterceptor(Logger logger) { + this.logger = logger; + } + + /** + * Change the level at which this interceptor logs. + * @param level log Level + * @return HttpLoggingInterceptor + */ + public HttpLoggingInterceptor setLevel(BladeLogLevel level) { + this.level = Objects.requireNonNull(level, "level == null. Use Level.NONE instead."); + return this; + } + + public BladeLogLevel getLevel() { + return level; + } + + @Override + public Response intercept(Chain chain) throws IOException { + BladeLogLevel level = this.level; + + Request request = chain.request(); + if (level == BladeLogLevel.NONE) { + return chain.proceed(request); + } + + boolean logBody = level == BladeLogLevel.BODY; + boolean logHeaders = logBody || level == BladeLogLevel.HEADERS; + + RequestBody requestBody = request.body(); + boolean hasRequestBody = requestBody != null; + + Connection connection = chain.connection(); + String requestStartMessage = "--> " + + request.method() + + ' ' + request.url() + + (connection != null ? " " + connection.protocol() : ""); + if (!logHeaders && hasRequestBody) { + requestStartMessage += " (" + requestBody.contentLength() + "-byte body)"; + } + logger.log(requestStartMessage); + + if (logHeaders) { + if (hasRequestBody) { + // Request body headers are only present when installed as a network interceptor. Force + // them to be included (when available) so there values are known. + if (requestBody.contentType() != null) { + logger.log("Content-Type: " + requestBody.contentType()); + } + if (requestBody.contentLength() != -1) { + logger.log("Content-Length: " + requestBody.contentLength()); + } + } + + Headers headers = request.headers(); + for (int i = 0, count = headers.size(); i < count; i++) { + String name = headers.name(i); + // Skip headers from the request body as they are explicitly logged above. + if (!"Content-Type".equalsIgnoreCase(name) && !"Content-Length".equalsIgnoreCase(name)) { + logger.log(name + ": " + headers.value(i)); + } + } + + if (!logBody || !hasRequestBody) { + logger.log("--> END " + request.method()); + } else if (bodyHasUnknownEncoding(request.headers())) { + logger.log("--> END " + request.method() + " (encoded body omitted)"); + } else { + Buffer buffer = new Buffer(); + requestBody.writeTo(buffer); + + Charset charset = UTF8; + MediaType contentType = requestBody.contentType(); + if (contentType != null) { + charset = contentType.charset(UTF8); + } + + logger.log(""); + if (isPlaintext(buffer)) { + logger.log(buffer.readString(charset)); + logger.log("--> END " + request.method() + + " (" + requestBody.contentLength() + "-byte body)"); + } else { + logger.log("--> END " + request.method() + " (binary " + + requestBody.contentLength() + "-byte body omitted)"); + } + } + } + + long startNs = System.nanoTime(); + Response response; + try { + response = chain.proceed(request); + } catch (Exception e) { + logger.log("<-- HTTP FAILED: " + e); + throw e; + } + long tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs); + + ResponseBody responseBody = response.body(); + long contentLength = responseBody.contentLength(); + String bodySize = contentLength != -1 ? contentLength + "-byte" : "unknown-length"; + logger.log("<-- " + + response.code() + + (response.message().isEmpty() ? "" : ' ' + response.message()) + + ' ' + response.request().url() + + " (" + tookMs + "ms" + (!logHeaders ? ", " + bodySize + " body" : "") + ')'); + + if (logHeaders) { + Headers headers = response.headers(); + int count = headers.size(); + for (int i = 0; i < count; i++) { + logger.log(headers.name(i) + ": " + headers.value(i)); + } + + if (!logBody || !HttpHeaders.hasBody(response)) { + logger.log("<-- END HTTP"); + } else if (bodyHasUnknownEncoding(response.headers())) { + logger.log("<-- END HTTP (encoded body omitted)"); + } else { + BufferedSource source = responseBody.source(); + // Buffer the entire body. + source.request(Long.MAX_VALUE); + Buffer buffer = source.getBuffer(); + + Long gzippedLength = null; + if ("gzip".equalsIgnoreCase(headers.get("Content-Encoding"))) { + gzippedLength = buffer.size(); + GzipSource gzippedResponseBody = null; + try { + gzippedResponseBody = new GzipSource(buffer.clone()); + buffer = new Buffer(); + buffer.writeAll(gzippedResponseBody); + } finally { + if (gzippedResponseBody != null) { + gzippedResponseBody.close(); + } + } + } + + Charset charset = UTF8; + MediaType contentType = responseBody.contentType(); + if (contentType != null) { + charset = contentType.charset(UTF8); + } + + if (!isPlaintext(buffer)) { + logger.log(""); + logger.log("<-- END HTTP (binary " + buffer.size() + "-byte body omitted)"); + return response; + } + + if (contentLength != 0) { + logger.log(""); + logger.log(buffer.clone().readString(charset)); + } + + if (gzippedLength != null) { + logger.log("<-- END HTTP (" + buffer.size() + "-byte, " + + gzippedLength + "-gzipped-byte body)"); + } else { + logger.log("<-- END HTTP (" + buffer.size() + "-byte body)"); + } + } + } + + return response; + } + + /** + * Returns true if the body in question probably contains human readable text. Uses a small sample + * of code points to detect unicode control characters commonly used in binary file signatures. + */ + private static boolean isPlaintext(Buffer buffer) { + try { + Buffer prefix = new Buffer(); + long byteCount = buffer.size() < 64 ? buffer.size() : 64; + buffer.copyTo(prefix, 0, byteCount); + for (int i = 0; i < 16; i++) { + if (prefix.exhausted()) { + break; + } + int codePoint = prefix.readUtf8CodePoint(); + if (Character.isISOControl(codePoint) && !Character.isWhitespace(codePoint)) { + return false; + } + } + return true; + } catch (EOFException e) { + // Truncated UTF-8 sequence. + return false; + } + } + + private boolean bodyHasUnknownEncoding(Headers headers) { + String contentEncoding = headers.get("Content-Encoding"); + return contentEncoding != null + && !"identity".equalsIgnoreCase(contentEncoding) + && !"gzip".equalsIgnoreCase(contentEncoding); + } +} diff --git a/blade-core-cloud/src/main/java/org/springblade/core/cloud/http/logger/OkHttpSlf4jLogger.java b/blade-core-cloud/src/main/java/org/springblade/core/cloud/http/logger/OkHttpSlf4jLogger.java new file mode 100644 index 0000000..50d4984 --- /dev/null +++ b/blade-core-cloud/src/main/java/org/springblade/core/cloud/http/logger/OkHttpSlf4jLogger.java @@ -0,0 +1,42 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.cloud.http.logger; + +import lombok.extern.slf4j.Slf4j; + +/** + * OkHttp Slf4j logger + * + * @author L.cm + */ +@Slf4j +public class OkHttpSlf4jLogger implements HttpLoggingInterceptor.Logger { + @Override + public void log(String message) { + log.info(message); + } +} diff --git a/blade-core-cloud/src/main/java/org/springblade/core/cloud/sentinel/BladeBlockExceptionHandler.java b/blade-core-cloud/src/main/java/org/springblade/core/cloud/sentinel/BladeBlockExceptionHandler.java new file mode 100644 index 0000000..159373c --- /dev/null +++ b/blade-core-cloud/src/main/java/org/springblade/core/cloud/sentinel/BladeBlockExceptionHandler.java @@ -0,0 +1,26 @@ +package org.springblade.core.cloud.sentinel; + +import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler; +import com.alibaba.csp.sentinel.slots.block.BlockException; +import org.springblade.core.tool.api.R; +import org.springblade.core.tool.jackson.JsonUtil; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +/** + * Sentinel统一限流策略 + * + * @author Chill + */ +public class BladeBlockExceptionHandler implements BlockExceptionHandler { + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception { + // Return 429 (Too Many Requests) by default. + response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value()); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.getWriter().print(JsonUtil.toJson(R.fail(e.getMessage()))); + } +} diff --git a/blade-core-cloud/src/main/java/org/springblade/core/cloud/sentinel/BladeFeignSentinel.java b/blade-core-cloud/src/main/java/org/springblade/core/cloud/sentinel/BladeFeignSentinel.java new file mode 100644 index 0000000..7dd956a --- /dev/null +++ b/blade-core-cloud/src/main/java/org/springblade/core/cloud/sentinel/BladeFeignSentinel.java @@ -0,0 +1,127 @@ +/* + * Copyright 2013-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.cloud.sentinel; + +import com.alibaba.cloud.sentinel.feign.SentinelContractHolder; +import feign.Contract; +import feign.Feign; +import feign.InvocationHandlerFactory; +import feign.Target; +import lombok.SneakyThrows; +import org.springblade.core.cloud.feign.BladeFallbackFactory; +import org.springframework.beans.BeansException; +import org.springframework.cloud.openfeign.FallbackFactory; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.cloud.openfeign.FeignClientFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.StringUtils; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.util.Map; + +/** + * feign集成sentinel自动配置 + * 重写 {@link com.alibaba.cloud.sentinel.feign.SentinelFeign} 适配最新API + * + * @author Chill + */ +public class BladeFeignSentinel { + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder extends Feign.Builder implements ApplicationContextAware { + private Contract contract = new Contract.Default(); + private ApplicationContext applicationContext; + private FeignClientFactory feignContext; + + @Override + public Feign.Builder invocationHandlerFactory(InvocationHandlerFactory invocationHandlerFactory) { + throw new UnsupportedOperationException(); + } + + @Override + public Builder contract(Contract contract) { + this.contract = contract; + return this; + } + + @Override + public Feign internalBuild() { + super.invocationHandlerFactory(new InvocationHandlerFactory() { + @SneakyThrows + @Override + public InvocationHandler create(Target target, Map dispatch) { + // 注解取值以避免循环依赖的问题 + FeignClient feignClient = AnnotationUtils.findAnnotation(target.type(), FeignClient.class); + Class fallback = feignClient.fallback(); + Class fallbackFactory = feignClient.fallbackFactory(); + String contextId = feignClient.contextId(); + + if (!StringUtils.hasText(contextId)) { + contextId = feignClient.name(); + } + + Object fallbackInstance; + FallbackFactory fallbackFactoryInstance; + // 判断fallback类型 + if (void.class != fallback) { + fallbackInstance = getFromContext(contextId, "fallback", fallback, target.type()); + return new BladeSentinelInvocationHandler(target, dispatch, new FallbackFactory.Default(fallbackInstance)); + } + if (void.class != fallbackFactory) { + fallbackFactoryInstance = (FallbackFactory) getFromContext(contextId, "fallbackFactory", fallbackFactory, FallbackFactory.class); + return new BladeSentinelInvocationHandler(target, dispatch, fallbackFactoryInstance); + } + // 默认fallbackFactory + BladeFallbackFactory bladeFallbackFactory = new BladeFallbackFactory(target); + return new BladeSentinelInvocationHandler(target, dispatch, bladeFallbackFactory); + } + + private Object getFromContext(String name, String type, Class fallbackType, Class targetType) { + Object fallbackInstance = feignContext.getInstance(name, fallbackType); + if (fallbackInstance == null) { + throw new IllegalStateException( + String.format("No %s instance of type %s found for feign client %s", + type, fallbackType, name) + ); + } + + if (!targetType.isAssignableFrom(fallbackType)) { + throw new IllegalStateException( + String.format("Incompatible %s instance. Fallback/fallbackFactory of type %s is not assignable to %s for feign client %s", + type, fallbackType, targetType, name) + ); + } + return fallbackInstance; + } + }); + super.contract(new SentinelContractHolder(contract)); + return super.internalBuild(); + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + feignContext = this.applicationContext.getBean(FeignClientFactory.class); + } + } + +} diff --git a/blade-core-cloud/src/main/java/org/springblade/core/cloud/sentinel/BladeSentinelAutoConfiguration.java b/blade-core-cloud/src/main/java/org/springblade/core/cloud/sentinel/BladeSentinelAutoConfiguration.java new file mode 100644 index 0000000..2d66519 --- /dev/null +++ b/blade-core-cloud/src/main/java/org/springblade/core/cloud/sentinel/BladeSentinelAutoConfiguration.java @@ -0,0 +1,71 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.cloud.sentinel; + +import com.alibaba.cloud.sentinel.feign.SentinelFeignAutoConfiguration; +import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler; +import feign.Feign; +import feign.RequestInterceptor; +import lombok.AllArgsConstructor; +import org.springblade.core.cloud.feign.BladeFeignRequestInterceptor; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; +import org.springframework.context.annotation.Scope; + +/** + * Sentinel配置类 + * + * @author Chill + */ +@AllArgsConstructor +@AutoConfiguration(before = SentinelFeignAutoConfiguration.class) +@ConditionalOnProperty(name = "feign.sentinel.enabled") +public class BladeSentinelAutoConfiguration { + + @Bean + @Primary + @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) + public Feign.Builder feignSentinelBuilder(RequestInterceptor requestInterceptor) { + return BladeFeignSentinel.builder().requestInterceptor(requestInterceptor); + } + + @Bean + @ConditionalOnMissingBean(name = "bladeFeignRequestInterceptor") + public RequestInterceptor requestInterceptor() { + return new BladeFeignRequestInterceptor(); + } + + @Bean + @ConditionalOnMissingBean + public BlockExceptionHandler blockExceptionHandler() { + return new BladeBlockExceptionHandler(); + } + +} diff --git a/blade-core-cloud/src/main/java/org/springblade/core/cloud/sentinel/BladeSentinelFilterConfiguration.java b/blade-core-cloud/src/main/java/org/springblade/core/cloud/sentinel/BladeSentinelFilterConfiguration.java new file mode 100644 index 0000000..b6709bb --- /dev/null +++ b/blade-core-cloud/src/main/java/org/springblade/core/cloud/sentinel/BladeSentinelFilterConfiguration.java @@ -0,0 +1,82 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.cloud.sentinel; + +import com.alibaba.cloud.sentinel.SentinelProperties; +import com.alibaba.csp.sentinel.adapter.spring.webmvc.SentinelWebInterceptor; +import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler; +import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.DefaultBlockExceptionHandler; +import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.RequestOriginParser; +import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.UrlCleaner; +import com.alibaba.csp.sentinel.adapter.spring.webmvc.config.SentinelWebMvcConfig; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.util.StringUtils; + +import java.util.Optional; + +/** + * 处理sentinel2021兼容问题 + * + * @author Chill + */ +@RequiredArgsConstructor +@Import(BladeSentinelFilterConfiguration.class) +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) +public class BladeSentinelFilterConfiguration { + + @Bean + public SentinelWebInterceptor sentinelWebInterceptor(SentinelWebMvcConfig sentinelWebMvcConfig) { + return new SentinelWebInterceptor(sentinelWebMvcConfig); + } + + @Bean + public SentinelWebMvcConfig sentinelWebMvcConfig(SentinelProperties properties, + Optional urlCleanerOptional, Optional blockExceptionHandlerOptional, + Optional requestOriginParserOptional) { + SentinelWebMvcConfig sentinelWebMvcConfig = new SentinelWebMvcConfig(); + sentinelWebMvcConfig.setHttpMethodSpecify(properties.getHttpMethodSpecify()); + sentinelWebMvcConfig.setWebContextUnify(properties.getWebContextUnify()); + + if (blockExceptionHandlerOptional.isPresent()) { + blockExceptionHandlerOptional.ifPresent(sentinelWebMvcConfig::setBlockExceptionHandler); + } else { + if (StringUtils.hasText(properties.getBlockPage())) { + sentinelWebMvcConfig.setBlockExceptionHandler( + ((request, response, e) -> response.sendRedirect(properties.getBlockPage()))); + } else { + sentinelWebMvcConfig.setBlockExceptionHandler(new DefaultBlockExceptionHandler()); + } + } + + urlCleanerOptional.ifPresent(sentinelWebMvcConfig::setUrlCleaner); + requestOriginParserOptional.ifPresent(sentinelWebMvcConfig::setOriginParser); + return sentinelWebMvcConfig; + } + +} diff --git a/blade-core-cloud/src/main/java/org/springblade/core/cloud/sentinel/BladeSentinelInvocationHandler.java b/blade-core-cloud/src/main/java/org/springblade/core/cloud/sentinel/BladeSentinelInvocationHandler.java new file mode 100644 index 0000000..50ce1ef --- /dev/null +++ b/blade-core-cloud/src/main/java/org/springblade/core/cloud/sentinel/BladeSentinelInvocationHandler.java @@ -0,0 +1,178 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.cloud.sentinel; + +import com.alibaba.cloud.sentinel.feign.SentinelContractHolder; +import com.alibaba.csp.sentinel.Entry; +import com.alibaba.csp.sentinel.EntryType; +import com.alibaba.csp.sentinel.SphU; +import com.alibaba.csp.sentinel.Tracer; +import com.alibaba.csp.sentinel.context.ContextUtil; +import com.alibaba.csp.sentinel.slots.block.BlockException; +import feign.Feign; +import feign.InvocationHandlerFactory; +import feign.MethodMetadata; +import feign.Target; +import org.springframework.cloud.openfeign.FallbackFactory; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.LinkedHashMap; +import java.util.Map; + +import static feign.Util.checkNotNull; + +/** + * 重写 {@link com.alibaba.cloud.sentinel.feign.SentinelInvocationHandler} 适配最新API + * + * @author Chill + */ +public class BladeSentinelInvocationHandler implements InvocationHandler { + + private final Target target; + + private final Map dispatch; + + private FallbackFactory fallbackFactory; + + private Map fallbackMethodMap; + + public BladeSentinelInvocationHandler(Target target, Map dispatch, + FallbackFactory fallbackFactory) { + this.target = checkNotNull(target, "target"); + this.dispatch = checkNotNull(dispatch, "dispatch"); + this.fallbackFactory = fallbackFactory; + this.fallbackMethodMap = toFallbackMethod(dispatch); + } + + public BladeSentinelInvocationHandler(Target target, Map dispatch) { + this.target = checkNotNull(target, "target"); + this.dispatch = checkNotNull(dispatch, "dispatch"); + } + + @Override + public Object invoke(final Object proxy, final Method method, final Object[] args) + throws Throwable { + if ("equals".equals(method.getName())) { + try { + Object otherHandler = args.length > 0 && args[0] != null + ? Proxy.getInvocationHandler(args[0]) : null; + return equals(otherHandler); + } catch (IllegalArgumentException e) { + return false; + } + } else if ("hashCode".equals(method.getName())) { + return hashCode(); + } else if ("toString".equals(method.getName())) { + return toString(); + } + + Object result; + InvocationHandlerFactory.MethodHandler methodHandler = this.dispatch.get(method); + // only handle by HardCodedTarget + if (target instanceof Target.HardCodedTarget) { + Target.HardCodedTarget hardCodedTarget = (Target.HardCodedTarget) target; + MethodMetadata methodMetadata = SentinelContractHolder.METADATA_MAP + .get(hardCodedTarget.type().getName() + + Feign.configKey(hardCodedTarget.type(), method)); + // resource default is HttpMethod:protocol://url + if (methodMetadata == null) { + result = methodHandler.invoke(args); + } else { + String resourceName = methodMetadata.template().method().toUpperCase() + + ":" + hardCodedTarget.url() + methodMetadata.template().path(); + Entry entry = null; + try { + ContextUtil.enter(resourceName); + entry = SphU.entry(resourceName, EntryType.OUT, 1, args); + result = methodHandler.invoke(args); + } catch (Throwable ex) { + // fallback handle + if (!BlockException.isBlockException(ex)) { + Tracer.trace(ex); + } + if (fallbackFactory != null) { + try { + Object fallbackResult = fallbackMethodMap.get(method) + .invoke(fallbackFactory.create(ex), args); + return fallbackResult; + } catch (IllegalAccessException e) { + // shouldn't happen as method is public due to being an + // interface + throw new AssertionError(e); + } catch (InvocationTargetException e) { + throw new AssertionError(e.getCause()); + } + } else { + // throw exception if fallbackFactory is null + throw ex; + } + } finally { + if (entry != null) { + entry.exit(1, args); + } + ContextUtil.exit(); + } + } + } else { + // other target type using default strategy + result = methodHandler.invoke(args); + } + + return result; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof BladeSentinelInvocationHandler) { + BladeSentinelInvocationHandler other = (BladeSentinelInvocationHandler) obj; + return target.equals(other.target); + } + return false; + } + + @Override + public int hashCode() { + return target.hashCode(); + } + + @Override + public String toString() { + return target.toString(); + } + + static Map toFallbackMethod(Map dispatch) { + Map result = new LinkedHashMap<>(); + for (Method method : dispatch.keySet()) { + method.setAccessible(true); + result.put(method, method); + } + return result; + } + +} diff --git a/blade-core-cloud/src/main/java/org/springblade/core/cloud/server/UndertowHttp2Configuration.java b/blade-core-cloud/src/main/java/org/springblade/core/cloud/server/UndertowHttp2Configuration.java new file mode 100644 index 0000000..bf54436 --- /dev/null +++ b/blade-core-cloud/src/main/java/org/springblade/core/cloud/server/UndertowHttp2Configuration.java @@ -0,0 +1,53 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.cloud.server; + +import io.undertow.Undertow; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration; +import org.springframework.boot.web.embedded.undertow.UndertowServletWebServerFactory; +import org.springframework.boot.web.server.WebServerFactoryCustomizer; +import org.springframework.context.annotation.Bean; + +import static io.undertow.UndertowOptions.ENABLE_HTTP2; + + +/** + * Undertow http2 h2c 配置,对 servlet 开启 + * + * @author L.cm + */ +@AutoConfiguration(before = ServletWebServerFactoryAutoConfiguration.class) +@ConditionalOnClass(Undertow.class) +public class UndertowHttp2Configuration { + + @Bean + public WebServerFactoryCustomizer undertowHttp2WebServerFactoryCustomizer() { + return factory -> factory.addBuilderCustomizers(builder -> builder.setServerOption(ENABLE_HTTP2, true)); + } + +} diff --git a/blade-core-cloud/src/main/java/org/springblade/core/cloud/version/BladeMediaType.java b/blade-core-cloud/src/main/java/org/springblade/core/cloud/version/BladeMediaType.java new file mode 100644 index 0000000..3f7c39e --- /dev/null +++ b/blade-core-cloud/src/main/java/org/springblade/core/cloud/version/BladeMediaType.java @@ -0,0 +1,58 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.cloud.version; + +import lombok.Getter; +import org.springframework.http.MediaType; + +/** + * blade Media Types,application/vnd.github.VERSION+json + * + *

+ * https://developer.github.com/v3/media/ + *

+ * + * @author L.cm + */ +@Getter +public class BladeMediaType { + private static final String MEDIA_TYPE_TEMP = "application/vnd.%s.%s+json"; + + private final String appName = "blade"; + private final String version; + private final MediaType mediaType; + + public BladeMediaType(String version) { + this.version = version; + this.mediaType = MediaType.valueOf(String.format(MEDIA_TYPE_TEMP, appName, version)); + } + + @Override + public String toString() { + return mediaType.toString(); + } +} diff --git a/blade-core-cloud/src/main/java/org/springblade/core/cloud/version/BladeRequestMappingHandlerMapping.java b/blade-core-cloud/src/main/java/org/springblade/core/cloud/version/BladeRequestMappingHandlerMapping.java new file mode 100644 index 0000000..9d3ebb6 --- /dev/null +++ b/blade-core-cloud/src/main/java/org/springblade/core/cloud/version/BladeRequestMappingHandlerMapping.java @@ -0,0 +1,113 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.cloud.version; + +import org.springblade.core.cloud.annotation.ApiVersion; +import org.springblade.core.cloud.annotation.UrlVersion; +import org.springblade.core.tool.utils.StringPool; +import org.springblade.core.tool.utils.StringUtil; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.lang.Nullable; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.mvc.method.RequestMappingInfo; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; + +import java.lang.reflect.Method; +import java.util.Map; + +/** + * url版本号处理 和 header 版本处理 + * + *

+ * url: /v1/user/{id} + * header: Accept application/vnd.blade.VERSION+json + *

+ * + * 注意:c 代表客户端版本 + * + * @author L.cm + */ +public class BladeRequestMappingHandlerMapping extends RequestMappingHandlerMapping { + + @Nullable + @Override + protected RequestMappingInfo getMappingForMethod(Method method, Class handlerType) { + RequestMappingInfo mappingInfo = super.getMappingForMethod(method, handlerType); + if (mappingInfo != null) { + RequestMappingInfo apiVersionMappingInfo = getApiVersionMappingInfo(method, handlerType); + return apiVersionMappingInfo == null ? mappingInfo : apiVersionMappingInfo.combine(mappingInfo); + } + return null; + } + + @Nullable + private RequestMappingInfo getApiVersionMappingInfo(Method method, Class handlerType) { + // url 上的版本,优先获取方法上的版本 + UrlVersion urlVersion = AnnotatedElementUtils.findMergedAnnotation(method, UrlVersion.class); + // 再次尝试类上的版本 + if (urlVersion == null || StringUtil.isBlank(urlVersion.value())) { + urlVersion = AnnotatedElementUtils.findMergedAnnotation(handlerType, UrlVersion.class); + } + // Media Types 版本信息 + ApiVersion apiVersion = AnnotatedElementUtils.findMergedAnnotation(method, ApiVersion.class); + // 再次尝试类上的版本 + if (apiVersion == null || StringUtil.isBlank(apiVersion.value())) { + apiVersion = AnnotatedElementUtils.findMergedAnnotation(handlerType, ApiVersion.class); + } + boolean nonUrlVersion = urlVersion == null || StringUtil.isBlank(urlVersion.value()); + boolean nonApiVersion = apiVersion == null || StringUtil.isBlank(apiVersion.value()); + // 先判断同时不纯在 + if (nonUrlVersion && nonApiVersion) { + return null; + } + // 如果 header 版本不存在 + RequestMappingInfo.Builder mappingInfoBuilder = null; + if (nonApiVersion) { + mappingInfoBuilder = RequestMappingInfo.paths(urlVersion.value()); + } else { + mappingInfoBuilder = RequestMappingInfo.paths(StringPool.EMPTY); + } + // 如果url版本不存在 + if (nonUrlVersion) { + String versionMediaTypes = new BladeMediaType(apiVersion.value()).toString(); + mappingInfoBuilder.produces(versionMediaTypes); + } + return mappingInfoBuilder.options(super.getBuilderConfiguration()).build(); + } + + @Override + protected void handlerMethodsInitialized(Map handlerMethods) { + // 打印路由信息 spring boot 2.1 去掉了这个 日志的打印 + if (logger.isInfoEnabled()) { + for (Map.Entry entry : handlerMethods.entrySet()) { + RequestMappingInfo mapping = entry.getKey(); + HandlerMethod handlerMethod = entry.getValue(); + logger.info("Mapped \"" + mapping + "\" onto " + handlerMethod); + } + } + super.handlerMethodsInitialized(handlerMethods); + } +} diff --git a/blade-core-cloud/src/main/java/org/springblade/core/cloud/version/BladeSpringMvcContract.java b/blade-core-cloud/src/main/java/org/springblade/core/cloud/version/BladeSpringMvcContract.java new file mode 100644 index 0000000..4a70c69 --- /dev/null +++ b/blade-core-cloud/src/main/java/org/springblade/core/cloud/version/BladeSpringMvcContract.java @@ -0,0 +1,110 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.cloud.version; + +import feign.MethodMetadata; +import org.springblade.core.cloud.annotation.ApiVersion; +import org.springblade.core.cloud.annotation.UrlVersion; +import org.springblade.core.tool.utils.StringPool; +import org.springblade.core.tool.utils.StringUtil; +import org.springframework.cloud.openfeign.AnnotatedParameterProcessor; +import org.springframework.cloud.openfeign.support.SpringMvcContract; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.core.convert.ConversionService; +import org.springframework.http.HttpHeaders; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.List; + +/** + * 支持 blade-boot 的 版本 处理 + * + * @see org.springblade.core.cloud.annotation.UrlVersion + * @see org.springblade.core.cloud.annotation.ApiVersion + * @author L.cm + */ +public class BladeSpringMvcContract extends SpringMvcContract { + + public BladeSpringMvcContract(List annotatedParameterProcessors, ConversionService conversionService) { + super(annotatedParameterProcessors, conversionService); + } + + @Override + protected void processAnnotationOnMethod(MethodMetadata data, Annotation methodAnnotation, Method method) { + if (RequestMapping.class.isInstance(methodAnnotation) || methodAnnotation.annotationType().isAnnotationPresent(RequestMapping.class)) { + Class targetType = method.getDeclaringClass(); + // url 上的版本,优先获取方法上的版本 + UrlVersion urlVersion = AnnotatedElementUtils.findMergedAnnotation(method, UrlVersion.class); + // 再次尝试类上的版本 + if (urlVersion == null || StringUtil.isBlank(urlVersion.value())) { + urlVersion = AnnotatedElementUtils.findMergedAnnotation(targetType, UrlVersion.class); + } + if (urlVersion != null && StringUtil.isNotBlank(urlVersion.value())) { + String versionUrl = "/" + urlVersion.value(); + data.template().uri(versionUrl); + } + + // 注意:在父类之前 添加 url版本,在父类之后,处理 Media Types 版本 + super.processAnnotationOnMethod(data, methodAnnotation, method); + + // 处理 Media Types 版本信息 + ApiVersion apiVersion = AnnotatedElementUtils.findMergedAnnotation(method, ApiVersion.class); + // 再次尝试类上的版本 + if (apiVersion == null || StringUtil.isBlank(apiVersion.value())) { + apiVersion = AnnotatedElementUtils.findMergedAnnotation(targetType, ApiVersion.class); + } + if (apiVersion != null && StringUtil.isNotBlank(apiVersion.value())) { + BladeMediaType bladeMediaType = new BladeMediaType(apiVersion.value()); + data.template().header(HttpHeaders.ACCEPT, bladeMediaType.toString()); + } + } + } + + /** + * 参考:https://gist.github.com/rmfish/0ed59a9af6c05157be2a60c9acea2a10 + * @param annotations 注解 + * @param paramIndex 参数索引 + * @return 是否 http 注解 + */ + @Override + protected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[] annotations, int paramIndex) { + boolean httpAnnotation = super.processAnnotationsOnParameter(data, annotations, paramIndex); + // 在 springMvc 中如果是 Get 请求且参数中是对象 没有声明为@RequestBody 则默认为 Param + if (!httpAnnotation && StringPool.GET.equals(data.template().method().toUpperCase())) { + for (Annotation parameterAnnotation : annotations) { + if (!(parameterAnnotation instanceof RequestBody)) { + return false; + } + } + data.queryMapIndex(paramIndex); + return true; + } + return httpAnnotation; + } +} diff --git a/blade-core-cloud/src/main/java/org/springblade/core/cloud/version/BladeWebMvcRegistrations.java b/blade-core-cloud/src/main/java/org/springblade/core/cloud/version/BladeWebMvcRegistrations.java new file mode 100644 index 0000000..2b794bc --- /dev/null +++ b/blade-core-cloud/src/main/java/org/springblade/core/cloud/version/BladeWebMvcRegistrations.java @@ -0,0 +1,53 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.cloud.version; + +import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations; +import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; + +/** + * url版本号处理 + * + * @author L.cm + */ +public class BladeWebMvcRegistrations implements WebMvcRegistrations { + @Override + public RequestMappingHandlerMapping getRequestMappingHandlerMapping() { + return new BladeRequestMappingHandlerMapping(); + } + + @Override + public RequestMappingHandlerAdapter getRequestMappingHandlerAdapter() { + return null; + } + + @Override + public ExceptionHandlerExceptionResolver getExceptionHandlerExceptionResolver() { + return null; + } +} diff --git a/blade-core-cloud/src/main/java/org/springblade/core/cloud/version/VersionMappingAutoConfiguration.java b/blade-core-cloud/src/main/java/org/springblade/core/cloud/version/VersionMappingAutoConfiguration.java new file mode 100644 index 0000000..1d9dfaa --- /dev/null +++ b/blade-core-cloud/src/main/java/org/springblade/core/cloud/version/VersionMappingAutoConfiguration.java @@ -0,0 +1,47 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.cloud.version; + +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations; +import org.springframework.context.annotation.Bean; + +/** + * url版本号处理 + * + * 参考:https://gitee.com/lianqu1990/spring-boot-starter-version-mapping + * + * @author L.cm + */ +@AutoConfiguration +@ConditionalOnWebApplication +public class VersionMappingAutoConfiguration { + @Bean + public WebMvcRegistrations bladeWebMvcRegistrations() { + return new BladeWebMvcRegistrations(); + } +} diff --git a/blade-core-context/pom.xml b/blade-core-context/pom.xml new file mode 100644 index 0000000..199f6b6 --- /dev/null +++ b/blade-core-context/pom.xml @@ -0,0 +1,31 @@ + + + 4.0.0 + + BladeX-Tool + org.springblade + ${revision} + + + blade-core-context + ${project.artifactId} + ${project.parent.version} + jar + + + + org.springblade + blade-core-tool + + + + org.springblade + blade-core-auto + provided + + + + + diff --git a/blade-core-context/src/main/java/org/springblade/core/context/BladeCallableWrapper.java b/blade-core-context/src/main/java/org/springblade/core/context/BladeCallableWrapper.java new file mode 100644 index 0000000..d4881d8 --- /dev/null +++ b/blade-core-context/src/main/java/org/springblade/core/context/BladeCallableWrapper.java @@ -0,0 +1,74 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.context; + +import org.slf4j.MDC; +import org.springblade.core.tool.utils.ThreadLocalUtil; +import org.springframework.lang.Nullable; + +import java.util.Map; +import java.util.concurrent.Callable; + +/** + * 多线程中传递 context 和 mdc + * + * @author L.cm + */ +public class BladeCallableWrapper implements Callable { + private final Callable delegate; + private final Map tlMap; + /** + * logback 下有可能为 null + */ + @Nullable + private final Map mdcMap; + + public BladeCallableWrapper(Callable callable) { + this.delegate = callable; + this.tlMap = ThreadLocalUtil.getAll(); + this.mdcMap = MDC.getCopyOfContextMap(); + } + + @Override + public V call() throws Exception { + if (!tlMap.isEmpty()) { + ThreadLocalUtil.put(tlMap); + } + if (mdcMap != null && !mdcMap.isEmpty()) { + MDC.setContextMap(mdcMap); + } + try { + return delegate.call(); + } finally { + tlMap.clear(); + if (mdcMap != null) { + mdcMap.clear(); + } + ThreadLocalUtil.clear(); + MDC.clear(); + } + } +} diff --git a/blade-core-context/src/main/java/org/springblade/core/context/BladeContext.java b/blade-core-context/src/main/java/org/springblade/core/context/BladeContext.java new file mode 100644 index 0000000..1dc16ce --- /dev/null +++ b/blade-core-context/src/main/java/org/springblade/core/context/BladeContext.java @@ -0,0 +1,82 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.context; + +import org.springframework.lang.Nullable; + +import java.util.function.Function; + +/** + * Blade微服务上下文 + * + * @author L.cm + */ +public interface BladeContext { + + /** + * 获取 请求 id + * + * @return 请求id + */ + @Nullable + String getRequestId(); + + /** + * 账号id + * + * @return 账号id + */ + @Nullable + String getAccountId(); + + /** + * 获取租户id + * + * @return 租户id + */ + @Nullable + String getTenantId(); + + /** + * 获取上下文中的数据 + * + * @param ctxKey 上下文中的key + * @return 返回对象 + */ + @Nullable + String get(String ctxKey); + + /** + * 获取上下文中的数据 + * + * @param ctxKey 上下文中的key + * @param function 函数式 + * @param 泛型对象 + * @return 返回对象 + */ + @Nullable + T get(String ctxKey, Function function); +} diff --git a/blade-core-context/src/main/java/org/springblade/core/context/BladeHttpHeadersGetter.java b/blade-core-context/src/main/java/org/springblade/core/context/BladeHttpHeadersGetter.java new file mode 100644 index 0000000..cf9bdf3 --- /dev/null +++ b/blade-core-context/src/main/java/org/springblade/core/context/BladeHttpHeadersGetter.java @@ -0,0 +1,59 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.context; + +import org.springframework.http.HttpHeaders; +import org.springframework.lang.Nullable; + +import jakarta.servlet.http.HttpServletRequest; + +/** + * HttpHeaders 获取器,用于跨服务和线程的传递, + *

+ * 暂时不支持 webflux。 + * + * @author L.cm + */ +public interface BladeHttpHeadersGetter { + + /** + * 获取 HttpHeaders + * + * @return HttpHeaders + */ + @Nullable + HttpHeaders get(); + + /** + * 获取 HttpHeaders + * + * @param request 请求 + * @return HttpHeaders + */ + @Nullable + HttpHeaders get(HttpServletRequest request); + +} diff --git a/blade-core-context/src/main/java/org/springblade/core/context/BladeRunnableWrapper.java b/blade-core-context/src/main/java/org/springblade/core/context/BladeRunnableWrapper.java new file mode 100644 index 0000000..faeb699 --- /dev/null +++ b/blade-core-context/src/main/java/org/springblade/core/context/BladeRunnableWrapper.java @@ -0,0 +1,73 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.context; + +import org.slf4j.MDC; +import org.springblade.core.tool.utils.ThreadLocalUtil; +import org.springframework.lang.Nullable; + +import java.util.Map; + +/** + * 多线程中传递 context 和 mdc + * + * @author L.cm + */ +public class BladeRunnableWrapper implements Runnable { + private final Runnable delegate; + private final Map tlMap; + /** + * logback 下有可能为 null + */ + @Nullable + private final Map mdcMap; + + public BladeRunnableWrapper(Runnable runnable) { + this.delegate = runnable; + this.tlMap = ThreadLocalUtil.getAll(); + this.mdcMap = MDC.getCopyOfContextMap(); + } + + @Override + public void run() { + if (!tlMap.isEmpty()) { + ThreadLocalUtil.put(tlMap); + } + if (mdcMap != null && !mdcMap.isEmpty()) { + MDC.setContextMap(mdcMap); + } + try { + delegate.run(); + } finally { + tlMap.clear(); + if (mdcMap != null) { + mdcMap.clear(); + } + ThreadLocalUtil.clear(); + MDC.clear(); + } + } +} diff --git a/blade-core-context/src/main/java/org/springblade/core/context/BladeServletContext.java b/blade-core-context/src/main/java/org/springblade/core/context/BladeServletContext.java new file mode 100644 index 0000000..c02fcc0 --- /dev/null +++ b/blade-core-context/src/main/java/org/springblade/core/context/BladeServletContext.java @@ -0,0 +1,89 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.context; + +import lombok.RequiredArgsConstructor; +import org.springblade.core.context.props.BladeContextProperties; +import org.springblade.core.tool.utils.StringUtil; +import org.springblade.core.tool.utils.ThreadLocalUtil; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.http.HttpHeaders; +import org.springframework.lang.Nullable; + +import java.util.function.Function; + +import static org.springblade.core.tool.constant.BladeConstant.CONTEXT_KEY; + +/** + * blade servlet 上下文,跨线程失效 + * + * @author L.cm + */ +@RequiredArgsConstructor +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) +public class BladeServletContext implements BladeContext { + private final BladeContextProperties contextProperties; + private final BladeHttpHeadersGetter httpHeadersGetter; + + @Nullable + @Override + public String getRequestId() { + return get(contextProperties.getHeaders().getRequestId()); + } + + @Nullable + @Override + public String getAccountId() { + return get(contextProperties.getHeaders().getAccountId()); + } + + @Nullable + @Override + public String getTenantId() { + return get(contextProperties.getHeaders().getTenantId()); + } + + @Nullable + @Override + public String get(String ctxKey) { + HttpHeaders headers = ThreadLocalUtil.getIfAbsent(CONTEXT_KEY, httpHeadersGetter::get); + if (headers == null || headers.isEmpty()) { + return null; + } + return headers.getFirst(ctxKey); + } + + @Nullable + @Override + public T get(String ctxKey, Function function) { + String ctxValue = get(ctxKey); + if (StringUtil.isBlank(ctxValue)) { + return null; + } + return function.apply(ctxKey); + } + +} diff --git a/blade-core-context/src/main/java/org/springblade/core/context/ServletHttpHeadersGetter.java b/blade-core-context/src/main/java/org/springblade/core/context/ServletHttpHeadersGetter.java new file mode 100644 index 0000000..13a0be0 --- /dev/null +++ b/blade-core-context/src/main/java/org/springblade/core/context/ServletHttpHeadersGetter.java @@ -0,0 +1,84 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.context; + +import lombok.RequiredArgsConstructor; +import org.springblade.core.context.props.BladeContextProperties; +import org.springblade.core.tool.utils.StringUtil; +import org.springblade.core.tool.utils.WebUtil; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.http.HttpHeaders; +import org.springframework.lang.Nullable; + +import jakarta.servlet.http.HttpServletRequest; +import java.util.Enumeration; +import java.util.List; + +/** + * HttpHeaders 获取器 + * + * @author L.cm + */ +@RequiredArgsConstructor +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) +public class ServletHttpHeadersGetter implements BladeHttpHeadersGetter { + private final BladeContextProperties properties; + + @Nullable + @Override + public HttpHeaders get() { + HttpServletRequest request = WebUtil.getRequest(); + if (request == null) { + return null; + } + return get(request); + } + + @Nullable + @Override + public HttpHeaders get(HttpServletRequest request) { + HttpHeaders headers = new HttpHeaders(); + List crossHeaders = properties.getCrossHeaders(); + // 传递请求头 + Enumeration headerNames = request.getHeaderNames(); + if (headerNames != null) { + List allowed = properties.getHeaders().getAllowed(); + while (headerNames.hasMoreElements()) { + String key = headerNames.nextElement(); + // 只支持配置的 header + if (crossHeaders.contains(key) || allowed.contains(key)) { + String values = request.getHeader(key); + // header value 不为空的 传递 + if (StringUtil.isNotBlank(values)) { + headers.add(key, values); + } + } + } + } + return headers; + } + +} diff --git a/blade-core-context/src/main/java/org/springblade/core/context/config/BladeContextAutoConfiguration.java b/blade-core-context/src/main/java/org/springblade/core/context/config/BladeContextAutoConfiguration.java new file mode 100644 index 0000000..03255b3 --- /dev/null +++ b/blade-core-context/src/main/java/org/springblade/core/context/config/BladeContextAutoConfiguration.java @@ -0,0 +1,62 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.context.config; + +import org.springblade.core.context.BladeContext; +import org.springblade.core.context.BladeHttpHeadersGetter; +import org.springblade.core.context.BladeServletContext; +import org.springblade.core.context.ServletHttpHeadersGetter; +import org.springblade.core.context.props.BladeContextProperties; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; + +/** + * blade 服务上下文配置 + * + * @author L.cm + */ +@AutoConfiguration +@Order(Ordered.HIGHEST_PRECEDENCE) +@EnableConfigurationProperties(BladeContextProperties.class) +public class BladeContextAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public BladeHttpHeadersGetter bladeHttpHeadersGetter(BladeContextProperties contextProperties) { + return new ServletHttpHeadersGetter(contextProperties); + } + + @Bean + @ConditionalOnMissingBean + public BladeContext bladeContext(BladeContextProperties contextProperties, BladeHttpHeadersGetter httpHeadersGetter) { + return new BladeServletContext(contextProperties, httpHeadersGetter); + } + +} diff --git a/blade-core-context/src/main/java/org/springblade/core/context/config/BladeServletListenerConfiguration.java b/blade-core-context/src/main/java/org/springblade/core/context/config/BladeServletListenerConfiguration.java new file mode 100644 index 0000000..4b47812 --- /dev/null +++ b/blade-core-context/src/main/java/org/springblade/core/context/config/BladeServletListenerConfiguration.java @@ -0,0 +1,51 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.context.config; + +import org.springblade.core.context.BladeHttpHeadersGetter; +import org.springblade.core.context.listener.BladeServletRequestListener; +import org.springblade.core.context.props.BladeContextProperties; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.web.servlet.ServletListenerRegistrationBean; +import org.springframework.context.annotation.Bean; + +/** + * Servlet 监听器自动配置 + * + * @author L.cm + */ +@AutoConfiguration +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) +public class BladeServletListenerConfiguration { + + @Bean + public ServletListenerRegistrationBean registerCustomListener(BladeContextProperties properties, + BladeHttpHeadersGetter httpHeadersGetter) { + return new ServletListenerRegistrationBean<>(new BladeServletRequestListener(properties, httpHeadersGetter)); + } + +} diff --git a/blade-core-context/src/main/java/org/springblade/core/context/listener/BladeServletRequestListener.java b/blade-core-context/src/main/java/org/springblade/core/context/listener/BladeServletRequestListener.java new file mode 100644 index 0000000..abf4b53 --- /dev/null +++ b/blade-core-context/src/main/java/org/springblade/core/context/listener/BladeServletRequestListener.java @@ -0,0 +1,83 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.context.listener; + +import lombok.RequiredArgsConstructor; +import org.slf4j.MDC; +import org.springblade.core.context.BladeHttpHeadersGetter; +import org.springblade.core.context.props.BladeContextProperties; +import org.springblade.core.tool.constant.BladeConstant; +import org.springblade.core.tool.utils.StringUtil; +import org.springblade.core.tool.utils.ThreadLocalUtil; +import org.springframework.http.HttpHeaders; + +import jakarta.servlet.ServletRequestEvent; +import jakarta.servlet.ServletRequestListener; +import jakarta.servlet.http.HttpServletRequest; + +/** + * Servlet 请求监听器 + * + * @author L.cm + */ +@RequiredArgsConstructor +public class BladeServletRequestListener implements ServletRequestListener { + private final BladeContextProperties contextProperties; + private final BladeHttpHeadersGetter httpHeadersGetter; + + @Override + public void requestInitialized(ServletRequestEvent event) { + HttpServletRequest request = (HttpServletRequest) event.getServletRequest(); + // MDC 获取透传的 变量 + BladeContextProperties.Headers headers = contextProperties.getHeaders(); + String requestId = request.getHeader(headers.getRequestId()); + if (StringUtil.isNotBlank(requestId)) { + MDC.put(BladeConstant.MDC_REQUEST_ID_KEY, requestId); + } + String accountId = request.getHeader(headers.getAccountId()); + if (StringUtil.isNotBlank(accountId)) { + MDC.put(BladeConstant.MDC_ACCOUNT_ID_KEY, accountId); + } + String tenantId = request.getHeader(headers.getTenantId()); + if (StringUtil.isNotBlank(tenantId)) { + MDC.put(BladeConstant.MDC_TENANT_ID_KEY, tenantId); + } + // 处理 context,直接传递 request,因为 spring 中的尚未初始化完成 + HttpHeaders httpHeaders = httpHeadersGetter.get(request); + ThreadLocalUtil.put(BladeConstant.CONTEXT_KEY, httpHeaders); + } + + @Override + public void requestDestroyed(ServletRequestEvent event) { + // 会话销毁时,清除上下文 + ThreadLocalUtil.clear(); + // 会话销毁时,清除 mdc + MDC.remove(BladeConstant.MDC_REQUEST_ID_KEY); + MDC.remove(BladeConstant.MDC_ACCOUNT_ID_KEY); + MDC.remove(BladeConstant.MDC_TENANT_ID_KEY); + } + +} diff --git a/blade-core-context/src/main/java/org/springblade/core/context/props/BladeContextProperties.java b/blade-core-context/src/main/java/org/springblade/core/context/props/BladeContextProperties.java new file mode 100644 index 0000000..d57232b --- /dev/null +++ b/blade-core-context/src/main/java/org/springblade/core/context/props/BladeContextProperties.java @@ -0,0 +1,90 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.context.props; + +import lombok.Getter; +import lombok.Setter; +import org.springblade.core.launch.constant.TokenConstant; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Headers 配置 + * + * @author L.cm + */ +@Getter +@Setter +@ConfigurationProperties(BladeContextProperties.PREFIX) +public class BladeContextProperties { + /** + * 配置前缀 + */ + public static final String PREFIX = "blade.context"; + /** + * 上下文传递的 headers 信息 + */ + private Headers headers = new Headers(); + + @Getter + @Setter + public static class Headers { + /** + * 请求id,默认:Blade-RequestId + */ + private String requestId = "Blade-RequestId"; + /** + * 用于 聚合层 向调用层传递用户信息 的请求头,默认:Blade-AccountId + */ + private String accountId = "Blade-AccountId"; + /** + * 用于 聚合层 向调用层传递租户id 的请求头,默认:Blade-TenantId + */ + private String tenantId = "Blade-TenantId"; + /** + * 自定义 RestTemplate 和 Feign 透传到下层的 Headers 名称列表 + */ + private List allowed = Arrays.asList("X-Real-IP", "x-forwarded-for", "version", "VERSION", "authorization", "Authorization", TokenConstant.HEADER.toLowerCase(), TokenConstant.HEADER); + } + + /** + * 获取跨服务的请求头 + * + * @return 请求头列表 + */ + public List getCrossHeaders() { + List headerList = new ArrayList<>(); + headerList.add(headers.getRequestId()); + headerList.add(headers.getAccountId()); + headerList.add(headers.getTenantId()); + headerList.addAll(headers.getAllowed()); + return headerList; + } + +} diff --git a/blade-core-db/pom.xml b/blade-core-db/pom.xml new file mode 100644 index 0000000..5a35663 --- /dev/null +++ b/blade-core-db/pom.xml @@ -0,0 +1,87 @@ + + + 4.0.0 + + BladeX-Tool + org.springblade + ${revision} + + + blade-core-db + ${project.artifactId} + ${project.parent.version} + jar + + + + + org.springblade + blade-core-tool + + + + org.springframework.boot + spring-boot-starter-jdbc + + + tomcat-jdbc + org.apache.tomcat + + + + + + com.baomidou + mybatis-plus-spring-boot3-starter + + + + com.alibaba + druid-spring-boot-3-starter + + + + com.mysql + mysql-connector-j + + + + com.oracle + ojdbc7 + true + + + + org.postgresql + postgresql + true + + + + com.microsoft.sqlserver + mssql-jdbc + true + + + + com.dameng + DmJdbcDriver18 + true + + + + com.yashandb.jdbc + yasdb-jdbc + true + + + + org.springblade + blade-core-auto + provided + + + + diff --git a/blade-core-db/src/main/java/org/springblade/core/db/config/DbConfiguration.java b/blade-core-db/src/main/java/org/springblade/core/db/config/DbConfiguration.java new file mode 100644 index 0000000..77366bc --- /dev/null +++ b/blade-core-db/src/main/java/org/springblade/core/db/config/DbConfiguration.java @@ -0,0 +1,40 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.db.config; + +import org.springblade.core.launch.props.BladePropertySource; +import org.springframework.boot.autoconfigure.AutoConfiguration; + +/** + * 数据源配置类 + * + * @author Chill + */ +@AutoConfiguration +@BladePropertySource(value = "classpath:/blade-db.yml") +public class DbConfiguration { + +} diff --git a/blade-core-db/src/main/java/org/springblade/core/db/package-info.java b/blade-core-db/src/main/java/org/springblade/core/db/package-info.java new file mode 100644 index 0000000..06ffb6a --- /dev/null +++ b/blade-core-db/src/main/java/org/springblade/core/db/package-info.java @@ -0,0 +1,6 @@ +/** + * Created by Blade. + * + * @author zhuangqian + */ +package org.springblade.core.db; diff --git a/blade-core-db/src/main/resources/blade-db.yml b/blade-core-db/src/main/resources/blade-db.yml new file mode 100644 index 0000000..cb28d17 --- /dev/null +++ b/blade-core-db/src/main/resources/blade-db.yml @@ -0,0 +1,39 @@ +#spring-datasource配置 +spring: + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + druid: + initial-size: 5 + max-active: 20 + min-idle: 5 + max-wait: 60000 + # MySql、PostgreSQL校验 + validation-query: select 1 + # Oracle校验 + #validation-query: select 1 from dual + validation-query-timeout: 2000 + test-on-borrow: false + test-on-return: false + test-while-idle: true + time-between-eviction-runs-millis: 60000 + min-evictable-idle-time-millis: 300000 + stat-view-servlet: + enabled: true + login-username: blade + login-password: 1qaz@WSX + web-stat-filter: + enabled: true + url-pattern: /* + exclusions: '*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*' + session-stat-enable: true + session-stat-max-count: 10 + #hikari: + #connection-test-query: SELECT 1 FROM DUAL + #connection-timeout: 30000 + #maximum-pool-size: 5 + #max-lifetime: 1800000 + #minimum-idle: 1 + + + + diff --git a/blade-core-launch/pom.xml b/blade-core-launch/pom.xml new file mode 100644 index 0000000..003e91b --- /dev/null +++ b/blade-core-launch/pom.xml @@ -0,0 +1,49 @@ + + + 4.0.0 + + BladeX-Tool + org.springblade + ${revision} + + + blade-core-launch + ${project.artifactId} + ${project.parent.version} + jar + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-tomcat + + + + + org.springframework.boot + spring-boot-starter-undertow + + + org.springframework.cloud + spring-cloud-starter-bootstrap + + + com.google.code.findbugs + jsr305 + + + + org.springblade + blade-core-auto + provided + + + + diff --git a/blade-core-launch/src/main/java/org/springblade/core/launch/BladeApplication.java b/blade-core-launch/src/main/java/org/springblade/core/launch/BladeApplication.java new file mode 100644 index 0000000..208230f --- /dev/null +++ b/blade-core-launch/src/main/java/org/springblade/core/launch/BladeApplication.java @@ -0,0 +1,223 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.launch; + +import org.springblade.core.launch.constant.AppConstant; +import org.springblade.core.launch.constant.NacosConstant; +import org.springblade.core.launch.service.LauncherService; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.env.*; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +import java.nio.charset.StandardCharsets; +import java.util.*; + +/** + * BladeX 应用启动类 + *

+ * 该类主要封装启动逻辑,包括环境变量处理、配置属性设置、以及自定义组件的加载。 + * + * @author BladeX + */ +public class BladeApplication { + + /** + * 启动SpringBoot应用,不使用自定义SpringApplicationBuilder + * + * @param appName 应用名称 + * @param source Spring Boot应用的主类 + * @param args 命令行参数 + * @return 配置完成的应用上下文 + */ + public static ConfigurableApplicationContext run(String appName, Class source, String... args) { + SpringApplicationBuilder springApplicationBuilder = createSpringApplicationBuilder(appName, source, args); + return springApplicationBuilder.run(args); + } + + /** + * 启动SpringBoot应用,使用自定义SpringApplicationBuilder + * + * @param appName 应用名称 + * @param source Spring Boot应用的主类 + * @param builder 自定义的SpringApplicationBuilder + * @param args 命令行参数 + * @return 配置完成的应用上下文 + */ + public static ConfigurableApplicationContext run(String appName, Class source, SpringApplicationBuilder builder, String... args) { + SpringApplicationBuilder springApplicationBuilder = createSpringApplicationBuilder(appName, source, builder, args); + return springApplicationBuilder.run(args); + } + + /** + * 创建SpringApplicationBuilder实例,包括环境配置、系统属性设置、默认属性设置和自定义组件加载 + * + * @param appName 应用名称 + * @param source Spring Boot应用的主类 + * @param args 命令行参数 + * @return 配置完成的SpringApplicationBuilder实例 + */ + public static SpringApplicationBuilder createSpringApplicationBuilder(String appName, Class source, String... args) { + return createSpringApplicationBuilder(appName, source, null, args); + } + + /** + * 创建SpringApplicationBuilder实例,包括环境配置、系统属性设置、默认属性设置和自定义组件加载 + * + * @param appName 应用名称 + * @param source Spring Boot应用的主类 + * @param builder 自定义的SpringApplicationBuilder + * @param args 命令行参数 + * @return 配置完成的SpringApplicationBuilder实例 + */ + public static SpringApplicationBuilder createSpringApplicationBuilder(String appName, Class source, SpringApplicationBuilder builder, String... args) { + Assert.hasText(appName, "[appName]服务名不能为空"); // 确保应用名称不为空 + ConfigurableEnvironment environment = configureEnvironment(args); // 配置环境变量,包括命令行参数、系统属性和系统环境变量 + List activeProfileList = getActiveProfiles(environment); // 获取当前激活的Spring配置文件列表 + String profile = determineActiveProfile(activeProfileList); // 确定要激活的配置文件,如果存在多个则抛出异常 + if (builder == null) { // 如果没有提供自定义的SpringApplicationBuilder,则创建一个新的 + builder = new SpringApplicationBuilder(source); // 使用Spring Boot应用的主类初始化SpringApplicationBuilder + } + builder.profiles(profile); // 设置SpringApplicationBuilder要激活的配置文件 + setSystemProperties(appName, profile); // 设置系统属性,包括应用名称、激活的配置文件等 + Properties defaultProperties = setDefaultProperties(appName, profile); // 设置默认的一些属性 + builder.properties(defaultProperties); // 将这些默认属性添加到SpringApplicationBuilder中 + loadCustomComponents(builder, appName, profile); // 加载自定义组件,如各种启动器服务 + return builder; // 返回配置好的SpringApplicationBuilder实例 + } + + /** + * 判断是否为本地开发环境 + * + * @return true如果是本地开发环境,否则为false + */ + public static boolean isLocalDev() { + String osName = System.getProperty("os.name"); + return StringUtils.hasText(osName) && !(AppConstant.OS_NAME_LINUX.equalsIgnoreCase(osName)); + } + + + /** + * 配置环境变量 + * + * @param args 命令行参数 + * @return 配置完成的环境 + */ + private static ConfigurableEnvironment configureEnvironment(String... args) { + ConfigurableEnvironment environment = new StandardEnvironment(); + MutablePropertySources propertySources = environment.getPropertySources(); + propertySources.addFirst(new SimpleCommandLinePropertySource(args)); + propertySources.addLast(new MapPropertySource(StandardEnvironment.SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, environment.getSystemProperties())); + propertySources.addLast(new SystemEnvironmentPropertySource(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, environment.getSystemEnvironment())); + return environment; + } + + /** + * 获取激活的配置文件列表 + * + * @param environment 环境配置 + * @return 激活的配置文件列表 + */ + private static List getActiveProfiles(ConfigurableEnvironment environment) { + String[] activeProfiles = environment.getActiveProfiles(); + return new ArrayList<>(Arrays.asList(activeProfiles)); + } + + /** + * 确定激活的配置文件 + * + * @param activeProfileList 激活的配置文件列表 + * @return 确定的激活配置文件 + */ + private static String determineActiveProfile(List activeProfileList) { + if (activeProfileList.isEmpty()) { + String defaultProfile = AppConstant.DEV_CODE; + activeProfileList.add(defaultProfile); + return defaultProfile; + } else if (activeProfileList.size() == 1) { + return activeProfileList.get(0); + } else { + throw new IllegalStateException("不可同时存在多个环境变量: " + String.join(", ", activeProfileList)); + } + } + + /** + * 设置系统属性 + * + * @param appName 应用名称 + * @param profile 激活的配置文件 + */ + private static void setSystemProperties(String appName, String profile) { + Properties props = System.getProperties(); + props.setProperty("spring.application.name", appName); + props.setProperty("spring.profiles.active", profile); + props.setProperty("info.version", AppConstant.APPLICATION_VERSION); + props.setProperty("info.desc", appName); + props.setProperty("file.encoding", StandardCharsets.UTF_8.name()); + props.setProperty("blade.env", profile); + props.setProperty("blade.name", appName); + props.setProperty("blade.is-local", String.valueOf(isLocalDev())); + props.setProperty("blade.dev-mode", profile.equals(AppConstant.PROD_CODE) ? "false" : "true"); + props.setProperty("blade.service.version", AppConstant.APPLICATION_VERSION); + props.setProperty("loadbalancer.client.name", appName); + } + + /** + * 设置默认属性 + * + * @param appName 应用名称 + * @param profile 激活的配置文件 + * @return 默认属性集 + */ + private static Properties setDefaultProperties(String appName, String profile) { + Properties defaultProperties = new Properties(); + defaultProperties.setProperty("nacos.logging.default.config.enabled", "false"); + defaultProperties.setProperty("spring.main.allow-bean-definition-overriding", "true"); + defaultProperties.setProperty("spring.sleuth.sampler.percentage", "1.0"); + defaultProperties.setProperty("spring.cloud.alibaba.seata.tx-service-group", appName.concat(NacosConstant.NACOS_GROUP_SUFFIX)); + defaultProperties.setProperty("spring.cloud.nacos.config.file-extension", NacosConstant.NACOS_CONFIG_FORMAT); + defaultProperties.setProperty("spring.cloud.nacos.config.shared-configs[0].data-id", NacosConstant.sharedDataId()); + defaultProperties.setProperty("spring.cloud.nacos.config.shared-configs[0].group", NacosConstant.NACOS_CONFIG_GROUP); + defaultProperties.setProperty("spring.cloud.nacos.config.shared-configs[0].refresh", NacosConstant.NACOS_CONFIG_REFRESH); + defaultProperties.setProperty("spring.cloud.nacos.config.shared-configs[1].data-id", NacosConstant.sharedDataId(profile)); + defaultProperties.setProperty("spring.cloud.nacos.config.shared-configs[1].group", NacosConstant.NACOS_CONFIG_GROUP); + defaultProperties.setProperty("spring.cloud.nacos.config.shared-configs[1].refresh", NacosConstant.NACOS_CONFIG_REFRESH); + return defaultProperties; + } + + /** + * 加载自定义组件 + * + * @param builder SpringApplicationBuilder实例 + * @param appName 应用名称 + * @param profile 激活的配置文件 + */ + private static void loadCustomComponents(SpringApplicationBuilder builder, String appName, String profile) { + ServiceLoader serviceLoader = ServiceLoader.load(LauncherService.class); + serviceLoader.forEach(launcherService -> launcherService.launcher(builder, appName, profile, isLocalDev())); + } +} diff --git a/blade-core-launch/src/main/java/org/springblade/core/launch/StartEventListener.java b/blade-core-launch/src/main/java/org/springblade/core/launch/StartEventListener.java new file mode 100644 index 0000000..bb8e27e --- /dev/null +++ b/blade-core-launch/src/main/java/org/springblade/core/launch/StartEventListener.java @@ -0,0 +1,58 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.launch; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.web.context.WebServerInitializedEvent; +import org.springframework.context.event.EventListener; +import org.springframework.core.annotation.Order; +import org.springframework.core.env.Environment; +import org.springframework.scheduling.annotation.Async; +import org.springframework.util.StringUtils; + +import java.util.Objects; + +/** + * 项目启动事件通知 + * + * @author Chill + */ +@Slf4j +@AutoConfiguration +public class StartEventListener { + + @Async + @Order + @EventListener(WebServerInitializedEvent.class) + public void afterStart(WebServerInitializedEvent event) { + Environment environment = event.getApplicationContext().getEnvironment(); + String appName = Objects.requireNonNull(environment.getProperty("spring.application.name")).toUpperCase(); + int localPort = event.getWebServer().getPort(); + String profile = StringUtils.arrayToCommaDelimitedString(environment.getActiveProfiles()); + log.info("---[{}]---启动完成,当前使用的端口:[{}],环境变量:[{}]---", appName, localPort, profile); + } +} diff --git a/blade-core-launch/src/main/java/org/springblade/core/launch/config/BladeLaunchConfiguration.java b/blade-core-launch/src/main/java/org/springblade/core/launch/config/BladeLaunchConfiguration.java new file mode 100644 index 0000000..923cd12 --- /dev/null +++ b/blade-core-launch/src/main/java/org/springblade/core/launch/config/BladeLaunchConfiguration.java @@ -0,0 +1,43 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.launch.config; + +import lombok.AllArgsConstructor; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; + +/** + * 配置类 + * + * @author Chill + */ +@AutoConfiguration +@AllArgsConstructor +@Order(Ordered.HIGHEST_PRECEDENCE) +public class BladeLaunchConfiguration { + +} diff --git a/blade-core-launch/src/main/java/org/springblade/core/launch/config/BladePropertyConfiguration.java b/blade-core-launch/src/main/java/org/springblade/core/launch/config/BladePropertyConfiguration.java new file mode 100644 index 0000000..52e16cf --- /dev/null +++ b/blade-core-launch/src/main/java/org/springblade/core/launch/config/BladePropertyConfiguration.java @@ -0,0 +1,52 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.launch.config; + +import org.springblade.core.launch.props.BladeProperties; +import org.springblade.core.launch.props.BladePropertySourcePostProcessor; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; + +/** + * blade property config + * + * @author L.cm + */ +@AutoConfiguration +@Order(Ordered.HIGHEST_PRECEDENCE) +@EnableConfigurationProperties(BladeProperties.class) +public class BladePropertyConfiguration { + + @Bean + public BladePropertySourcePostProcessor bladePropertySourcePostProcessor() { + return new BladePropertySourcePostProcessor(); + } + +} diff --git a/blade-core-launch/src/main/java/org/springblade/core/launch/constant/AppConstant.java b/blade-core-launch/src/main/java/org/springblade/core/launch/constant/AppConstant.java new file mode 100644 index 0000000..a5aafb4 --- /dev/null +++ b/blade-core-launch/src/main/java/org/springblade/core/launch/constant/AppConstant.java @@ -0,0 +1,144 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.launch.constant; + +/** + * 系统常量 + * + * @author Chill + */ +public interface AppConstant { + + /** + * 应用版本 + */ + String APPLICATION_VERSION = "4.0.1.RELEASE"; + + /** + * 基础包 + */ + String BASE_PACKAGES = "org.springblade"; + + /** + * 应用名前缀 + */ + String APPLICATION_NAME_PREFIX = "blade-"; + /** + * 网关模块名称 + */ + String APPLICATION_GATEWAY_NAME = APPLICATION_NAME_PREFIX + "gateway"; + /** + * 授权模块名称 + */ + String APPLICATION_AUTH_NAME = APPLICATION_NAME_PREFIX + "auth"; + /** + * 监控模块名称 + */ + String APPLICATION_ADMIN_NAME = APPLICATION_NAME_PREFIX + "admin"; + /** + * 报表系统名称 + */ + String APPLICATION_REPORT_NAME = APPLICATION_NAME_PREFIX + "report"; + /** + * 集群监控名称 + */ + String APPLICATION_TURBINE_NAME = APPLICATION_NAME_PREFIX + "turbine"; + /** + * 链路追踪名称 + */ + String APPLICATION_ZIPKIN_NAME = APPLICATION_NAME_PREFIX + "zipkin"; + /** + * websocket名称 + */ + String APPLICATION_WEBSOCKET_NAME = APPLICATION_NAME_PREFIX + "websocket"; + /** + * 首页模块名称 + */ + String APPLICATION_DESK_NAME = APPLICATION_NAME_PREFIX + "desk"; + /** + * 系统模块名称 + */ + String APPLICATION_SYSTEM_NAME = APPLICATION_NAME_PREFIX + "system"; + /** + * 用户模块名称 + */ + String APPLICATION_USER_NAME = APPLICATION_NAME_PREFIX + "user"; + /** + * 日志模块名称 + */ + String APPLICATION_LOG_NAME = APPLICATION_NAME_PREFIX + "log"; + /** + * 开发模块名称 + */ + String APPLICATION_DEVELOP_NAME = APPLICATION_NAME_PREFIX + "develop"; + /** + * 流程设计器模块名称 + */ + String APPLICATION_FLOWDESIGN_NAME = APPLICATION_NAME_PREFIX + "flowdesign"; + /** + * 工作流模块名称 + */ + String APPLICATION_FLOW_NAME = APPLICATION_NAME_PREFIX + "flow"; + /** + * 资源模块名称 + */ + String APPLICATION_RESOURCE_NAME = APPLICATION_NAME_PREFIX + "resource"; + /** + * 接口文档模块名称 + */ + String APPLICATION_SWAGGER_NAME = APPLICATION_NAME_PREFIX + "swagger"; + /** + * 任务模块名称 + */ + String APPLICATION_JOB_NAME = APPLICATION_NAME_PREFIX + "job"; + /** + * 测试模块名称 + */ + String APPLICATION_TEST_NAME = APPLICATION_NAME_PREFIX + "test"; + /** + * 演示模块名称 + */ + String APPLICATION_DEMO_NAME = APPLICATION_NAME_PREFIX + "demo"; + + /** + * 开发环境 + */ + String DEV_CODE = "dev"; + /** + * 生产环境 + */ + String PROD_CODE = "prod"; + /** + * 测试环境 + */ + String TEST_CODE = "test"; + + /** + * 代码部署于 linux 上,工作默认为 mac 和 Windows + */ + String OS_NAME_LINUX = "LINUX"; + +} diff --git a/blade-core-launch/src/main/java/org/springblade/core/launch/constant/ConsulConstant.java b/blade-core-launch/src/main/java/org/springblade/core/launch/constant/ConsulConstant.java new file mode 100644 index 0000000..71ab6e6 --- /dev/null +++ b/blade-core-launch/src/main/java/org/springblade/core/launch/constant/ConsulConstant.java @@ -0,0 +1,59 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.launch.constant; + +/** + * Consul常量. + * + * @author Chill + */ +public interface ConsulConstant { + + /** + * consul dev 地址 + */ + String CONSUL_HOST = "http://localhost"; + + /** + * consul端口 + */ + String CONSUL_PORT = "8500"; + + /** + * consul端口 + */ + String CONSUL_CONFIG_FORMAT = "yaml"; + + /** + * consul端口 + */ + String CONSUL_WATCH_DELAY = "1000"; + + /** + * consul端口 + */ + String CONSUL_WATCH_ENABLED = "true"; +} diff --git a/blade-core-launch/src/main/java/org/springblade/core/launch/constant/FlowConstant.java b/blade-core-launch/src/main/java/org/springblade/core/launch/constant/FlowConstant.java new file mode 100644 index 0000000..5caa859 --- /dev/null +++ b/blade-core-launch/src/main/java/org/springblade/core/launch/constant/FlowConstant.java @@ -0,0 +1,40 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.launch.constant; + +/** + * 流程常量. + * + * @author Chill + */ +public interface FlowConstant { + + /** + * 任务用户标识前缀 + */ + String TASK_USR_PREFIX = "taskUser_"; + +} diff --git a/blade-core-launch/src/main/java/org/springblade/core/launch/constant/NacosConstant.java b/blade-core-launch/src/main/java/org/springblade/core/launch/constant/NacosConstant.java new file mode 100644 index 0000000..5ddd976 --- /dev/null +++ b/blade-core-launch/src/main/java/org/springblade/core/launch/constant/NacosConstant.java @@ -0,0 +1,137 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.launch.constant; + +/** + * Nacos常量. + * + * @author Chill + */ +public interface NacosConstant { + + /** + * nacos 地址 + */ + String NACOS_ADDR = "127.0.0.1:8848"; + + /** + * nacos 配置前缀 + */ + String NACOS_CONFIG_PREFIX = "blade"; + + /** + * nacos 组配置后缀 + */ + String NACOS_GROUP_SUFFIX = "-group"; + + /** + * nacos 配置文件类型 + */ + String NACOS_CONFIG_FORMAT = "yaml"; + + /** + * nacos json配置文件类型 + */ + String NACOS_CONFIG_JSON_FORMAT = "json"; + + /** + * nacos 是否刷新 + */ + String NACOS_CONFIG_REFRESH = "true"; + + /** + * nacos 分组 + */ + String NACOS_CONFIG_GROUP = "DEFAULT_GROUP"; + + /** + * seata 分组 + */ + String NACOS_SEATA_GROUP = "SEATA_GROUP"; + + /** + * 构建服务对应的 dataId + * + * @param appName 服务名 + * @return dataId + */ + static String dataId(String appName) { + return appName + "." + NACOS_CONFIG_FORMAT; + } + + /** + * 构建服务对应的 dataId + * + * @param appName 服务名 + * @param profile 环境变量 + * @return dataId + */ + static String dataId(String appName, String profile) { + return dataId(appName, profile, NACOS_CONFIG_FORMAT); + } + + /** + * 构建服务对应的 dataId + * + * @param appName 服务名 + * @param profile 环境变量 + * @param format 文件类型 + * @return dataId + */ + static String dataId(String appName, String profile, String format) { + return appName + "-" + profile + "." + format; + } + + /** + * 服务默认加载的配置 + * + * @return sharedDataIds + */ + static String sharedDataId() { + return NACOS_CONFIG_PREFIX + "." + NACOS_CONFIG_FORMAT; + } + + /** + * 服务默认加载的配置 + * + * @param profile 环境变量 + * @return sharedDataIds + */ + static String sharedDataId(String profile) { + return NACOS_CONFIG_PREFIX + "-" + profile + "." + NACOS_CONFIG_FORMAT; + } + + /** + * 服务默认加载的配置 + * + * @param profile 环境变量 + * @return sharedDataIds + */ + static String sharedDataIds(String profile) { + return NACOS_CONFIG_PREFIX + "." + NACOS_CONFIG_FORMAT + "," + NACOS_CONFIG_PREFIX + "-" + profile + "." + NACOS_CONFIG_FORMAT; + } + +} diff --git a/blade-core-launch/src/main/java/org/springblade/core/launch/constant/SentinelConstant.java b/blade-core-launch/src/main/java/org/springblade/core/launch/constant/SentinelConstant.java new file mode 100644 index 0000000..04837c7 --- /dev/null +++ b/blade-core-launch/src/main/java/org/springblade/core/launch/constant/SentinelConstant.java @@ -0,0 +1,39 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.launch.constant; + +/** + * sentinel配置. + * + * @author Chill + */ +public interface SentinelConstant { + + /** + * sentinel 地址 + */ + String SENTINEL_ADDR = "127.0.0.1:8858"; +} diff --git a/blade-core-launch/src/main/java/org/springblade/core/launch/constant/TokenConstant.java b/blade-core-launch/src/main/java/org/springblade/core/launch/constant/TokenConstant.java new file mode 100644 index 0000000..72098f7 --- /dev/null +++ b/blade-core-launch/src/main/java/org/springblade/core/launch/constant/TokenConstant.java @@ -0,0 +1,62 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.launch.constant; + +/** + * Token配置常量. + * + * @author Chill + */ +public interface TokenConstant { + + String AVATAR = "avatar"; + String HEADER = "Blade-Auth"; + String BEARER = "bearer"; + String ACCESS_TOKEN = "access_token"; + String REFRESH_TOKEN = "refresh_token"; + String CLIENT_ACCESS_TOKEN = "client_access_token"; + String IMPLICIT_ACCESS_TOKEN = "implicit_access_token"; + String TOKEN_TYPE = "token_type"; + String EXPIRES_IN = "expires_in"; + String ACCOUNT = "account"; + String USER_NAME = "user_name"; + String NICK_NAME = "nick_name"; + String REAL_NAME = "real_name"; + String USER_ID = "user_id"; + String DEPT_ID = "dept_id"; + String POST_ID = "post_id"; + String ROLE_ID = "role_id"; + String ROLE_NAME = "role_name"; + String TENANT_ID = "tenant_id"; + String OAUTH_ID = "oauth_id"; + String CLIENT_ID = "client_id"; + String DETAIL = "detail"; + String LICENSE = "license"; + String LICENSE_NAME = "powered by bladex"; + String DEFAULT_AVATAR = "https://bladex.cn/images/logo.png"; + Integer AUTH_LENGTH = 7; + +} diff --git a/blade-core-launch/src/main/java/org/springblade/core/launch/constant/ZookeeperConstant.java b/blade-core-launch/src/main/java/org/springblade/core/launch/constant/ZookeeperConstant.java new file mode 100644 index 0000000..5bca9a4 --- /dev/null +++ b/blade-core-launch/src/main/java/org/springblade/core/launch/constant/ZookeeperConstant.java @@ -0,0 +1,54 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.launch.constant; + +/** + * zookeeper 配置. + * + * @author Chill + */ +public interface ZookeeperConstant { + + /** + * zookeeper id + */ + String ZOOKEEPER_ID = "zk"; + + /** + * zookeeper connect string + */ + String ZOOKEEPER_CONNECT_STRING = "127.0.0.1:2181"; + + /** + * zookeeper address + */ + String ZOOKEEPER_ADDRESS = "zookeeper://" + ZOOKEEPER_CONNECT_STRING; + + /** + * zookeeper root + */ + String ZOOKEEPER_ROOT = "/blade-services"; +} diff --git a/blade-core-launch/src/main/java/org/springblade/core/launch/log/BladeLogLevel.java b/blade-core-launch/src/main/java/org/springblade/core/launch/log/BladeLogLevel.java new file mode 100644 index 0000000..d1b569b --- /dev/null +++ b/blade-core-launch/src/main/java/org/springblade/core/launch/log/BladeLogLevel.java @@ -0,0 +1,123 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.launch.log; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * 请求日志级别,来源 okHttp + * + * @author L.cm + */ +@Getter +@RequiredArgsConstructor +public enum BladeLogLevel { + /** + * No logs. + */ + NONE(0), + + /** + * Logs request and response lines. + * + *

Example: + *

{@code
+	 * --> POST /greeting http/1.1 (3-byte body)
+	 *
+	 * <-- 200 OK (22ms, 6-byte body)
+	 * }
+ */ + BASIC(1), + + /** + * Logs request and response lines and their respective headers. + * + *

Example: + *

{@code
+	 * --> POST /greeting http/1.1
+	 * Host: example.com
+	 * Content-Type: plain/text
+	 * Content-Length: 3
+	 * --> END POST
+	 *
+	 * <-- 200 OK (22ms)
+	 * Content-Type: plain/text
+	 * Content-Length: 6
+	 * <-- END HTTP
+	 * }
+ */ + HEADERS(2), + + /** + * Logs request and response lines and their respective headers and bodies (if present). + * + *

Example: + *

{@code
+	 * --> POST /greeting http/1.1
+	 * Host: example.com
+	 * Content-Type: plain/text
+	 * Content-Length: 3
+	 *
+	 * Hi?
+	 * --> END POST
+	 *
+	 * <-- 200 OK (22ms)
+	 * Content-Type: plain/text
+	 * Content-Length: 6
+	 *
+	 * Hello!
+	 * <-- END HTTP
+	 * }
+ */ + BODY(3); + + /** + * 请求日志配置前缀 + */ + public static final String REQ_LOG_PROPS_PREFIX = "blade.log.request"; + /** + * 控制台日志是否启用 + */ + public static final String CONSOLE_LOG_ENABLED_PROP = "blade.log.console.enabled"; + + /** + * 级别 + */ + private final int level; + + /** + * 当前版本 小于和等于 比较的版本 + * + * @param level LogLevel + * @return 是否小于和等于 + */ + public boolean lte(BladeLogLevel level) { + return this.level <= level.level; + } + +} diff --git a/blade-core-launch/src/main/java/org/springblade/core/launch/props/BladeProperties.java b/blade-core-launch/src/main/java/org/springblade/core/launch/props/BladeProperties.java new file mode 100644 index 0000000..7c1bb86 --- /dev/null +++ b/blade-core-launch/src/main/java/org/springblade/core/launch/props/BladeProperties.java @@ -0,0 +1,263 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.launch.props; + +import lombok.Getter; +import org.springblade.core.launch.constant.AppConstant; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.EnvironmentAware; +import org.springframework.core.env.Environment; +import org.springframework.core.env.EnvironmentCapable; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +/** + * 配置文件 + * + * @author Chill + */ +@ConfigurationProperties("blade") +public class BladeProperties implements EnvironmentAware, EnvironmentCapable { + @Nullable + private Environment environment; + + /** + * 装载自定义配置blade.prop.xxx + */ + @Getter + private final Map prop = new HashMap<>(); + + /** + * 获取配置 + * + * @param key key + * @return value + */ + @Nullable + public String get(String key) { + return get(key, null); + } + + /** + * 获取配置 + * + * @param key key + * @param defaultValue 默认值 + * @return value + */ + @Nullable + public String get(String key, @Nullable String defaultValue) { + String value = prop.get(key); + if (value == null) { + return defaultValue; + } + return value; + } + + /** + * 获取配置 + * + * @param key key + * @return int value + */ + @Nullable + public Integer getInt(String key) { + return getInt(key, null); + } + + /** + * 获取配置 + * + * @param key key + * @param defaultValue 默认值 + * @return int value + */ + @Nullable + public Integer getInt(String key, @Nullable Integer defaultValue) { + String value = prop.get(key); + if (value != null) { + return Integer.valueOf(value.trim()); + } + return defaultValue; + } + + /** + * 获取配置 + * + * @param key key + * @return long value + */ + @Nullable + public Long getLong(String key) { + return getLong(key, null); + } + + /** + * 获取配置 + * + * @param key key + * @param defaultValue 默认值 + * @return long value + */ + @Nullable + public Long getLong(String key, @Nullable Long defaultValue) { + String value = prop.get(key); + if (value != null) { + return Long.valueOf(value.trim()); + } + return defaultValue; + } + + /** + * 获取配置 + * + * @param key key + * @return Boolean value + */ + @Nullable + public Boolean getBoolean(String key) { + return getBoolean(key, null); + } + + /** + * 获取配置 + * + * @param key key + * @param defaultValue 默认值 + * @return Boolean value + */ + @Nullable + public Boolean getBoolean(String key, @Nullable Boolean defaultValue) { + String value = prop.get(key); + if (value != null) { + value = value.toLowerCase().trim(); + return Boolean.parseBoolean(value); + } + return defaultValue; + } + + /** + * 获取配置 + * + * @param key key + * @return double value + */ + @Nullable + public Double getDouble(String key) { + return getDouble(key, null); + } + + /** + * 获取配置 + * + * @param key key + * @param defaultValue 默认值 + * @return double value + */ + @Nullable + public Double getDouble(String key, @Nullable Double defaultValue) { + String value = prop.get(key); + if (value != null) { + return Double.parseDouble(value.trim()); + } + return defaultValue; + } + + /** + * 判断是否存在key + * + * @param key prop key + * @return boolean + */ + public boolean containsKey(String key) { + return prop.containsKey(key); + } + + + /** + * 环境,方便在代码中获取 + * + * @return 环境 env + */ + public String getEnv() { + Objects.requireNonNull(environment, "Spring boot 环境下 Environment 不可能为null"); + String env = environment.getProperty("blade.env"); + Assert.notNull(env, "请使用 BladeApplication 启动..."); + return env; + } + + /** + * 是否是开发环境 + * + * @return boolean + */ + public boolean isDev() { + return AppConstant.DEV_CODE.equals(getEnv()); + } + + /** + * 是否是生产环境 + * + * @return boolean + */ + public boolean isProd() { + return AppConstant.PROD_CODE.equals(getEnv()); + } + + /** + * 是否是测试环境 + * + * @return boolean + */ + public boolean isTest() { + return AppConstant.TEST_CODE.equals(getEnv()); + } + + /** + * 应用名称${spring.application.name} + * + * @return 应用名 + */ + public String getName() { + Objects.requireNonNull(environment, "Spring boot 环境下 Environment 不可能为null"); + return environment.getProperty("spring.application.name", environment.getProperty("blade.name", "")); + } + + @Override + public void setEnvironment(Environment environment) { + this.environment = environment; + } + + @Override + public Environment getEnvironment() { + Objects.requireNonNull(environment, "Spring boot 环境下 Environment 不可能为null"); + return this.environment; + } +} diff --git a/blade-core-launch/src/main/java/org/springblade/core/launch/props/BladePropertySource.java b/blade-core-launch/src/main/java/org/springblade/core/launch/props/BladePropertySource.java new file mode 100644 index 0000000..9d236d5 --- /dev/null +++ b/blade-core-launch/src/main/java/org/springblade/core/launch/props/BladePropertySource.java @@ -0,0 +1,65 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.launch.props; + +import org.springframework.core.Ordered; + +import java.lang.annotation.*; + +/** + * 自定义资源文件读取,优先级最低 + * + * @author L.cm + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface BladePropertySource { + + /** + * Indicate the resource location(s) of the properties file to be loaded. + * for example, {@code "classpath:/com/example/app.yml"} + * + * @return location(s) + */ + String value(); + + /** + * load app-{activeProfile}.yml + * + * @return {boolean} + */ + boolean loadActiveProfile() default true; + + /** + * Get the order value of this resource. + * + * @return order + */ + int order() default Ordered.LOWEST_PRECEDENCE; + +} diff --git a/blade-core-launch/src/main/java/org/springblade/core/launch/props/BladePropertySourcePostProcessor.java b/blade-core-launch/src/main/java/org/springblade/core/launch/props/BladePropertySourcePostProcessor.java new file mode 100644 index 0000000..76d56ca --- /dev/null +++ b/blade-core-launch/src/main/java/org/springblade/core/launch/props/BladePropertySourcePostProcessor.java @@ -0,0 +1,188 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.launch.props; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.boot.env.PropertySourceLoader; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.core.env.PropertySource; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.support.SpringFactoriesLoader; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +import java.io.IOException; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 自定义资源文件读取,优先级最低 + * + * @author L.cm + */ +@Slf4j +public class BladePropertySourcePostProcessor implements BeanFactoryPostProcessor, InitializingBean, Ordered { + private final ResourceLoader resourceLoader; + private final List propertySourceLoaders; + + public BladePropertySourcePostProcessor() { + this.resourceLoader = new DefaultResourceLoader(); + this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class, getClass().getClassLoader()); + } + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + log.info("BladePropertySourcePostProcessor process @BladePropertySource bean."); + Map beansWithAnnotation = beanFactory.getBeansWithAnnotation(BladePropertySource.class); + Set> beanEntrySet = beansWithAnnotation.entrySet(); + // 没有 @YmlPropertySource 注解,跳出 + if (beanEntrySet.isEmpty()) { + log.warn("Not found @BladePropertySource on spring bean class."); + return; + } + // 组装资源 + List propertyFileList = new ArrayList<>(); + for (Map.Entry entry : beanEntrySet) { + Class beanClass = ClassUtils.getUserClass(entry.getValue()); + BladePropertySource propertySource = AnnotationUtils.getAnnotation(beanClass, BladePropertySource.class); + if (propertySource == null) { + continue; + } + int order = propertySource.order(); + boolean loadActiveProfile = propertySource.loadActiveProfile(); + String location = propertySource.value(); + propertyFileList.add(new PropertyFile(order, location, loadActiveProfile)); + } + + // 装载 PropertySourceLoader + Map loaderMap = new HashMap<>(16); + for (PropertySourceLoader loader : propertySourceLoaders) { + String[] loaderExtensions = loader.getFileExtensions(); + for (String extension : loaderExtensions) { + loaderMap.put(extension, loader); + } + } + // 去重,排序 + List sortedPropertyList = propertyFileList.stream() + .distinct() + .sorted() + .collect(Collectors.toList()); + ConfigurableEnvironment environment = beanFactory.getBean(ConfigurableEnvironment.class); + MutablePropertySources propertySources = environment.getPropertySources(); + + // 只支持 activeProfiles,没有必要支持 spring.profiles.include。 + String[] activeProfiles = environment.getActiveProfiles(); + ArrayList propertySourceList = new ArrayList<>(); + for (String profile : activeProfiles) { + for (PropertyFile propertyFile : sortedPropertyList) { + // 不加载 ActiveProfile 的配置文件 + if (!propertyFile.loadActiveProfile) { + continue; + } + String extension = propertyFile.getExtension(); + PropertySourceLoader loader = loaderMap.get(extension); + if (loader == null) { + throw new IllegalArgumentException("Can't find PropertySourceLoader for PropertySource extension:" + extension); + } + String location = propertyFile.getLocation(); + String filePath = StringUtils.stripFilenameExtension(location); + String profiledLocation = filePath + "-" + profile + "." + extension; + Resource resource = resourceLoader.getResource(profiledLocation); + loadPropertySource(profiledLocation, resource, loader, propertySourceList); + } + } + // 本身的 Resource + for (PropertyFile propertyFile : sortedPropertyList) { + String extension = propertyFile.getExtension(); + PropertySourceLoader loader = loaderMap.get(extension); + String location = propertyFile.getLocation(); + Resource resource = resourceLoader.getResource(location); + loadPropertySource(location, resource, loader, propertySourceList); + } + // 转存 + for (PropertySource propertySource : propertySourceList) { + propertySources.addLast(propertySource); + } + } + + private static void loadPropertySource(String location, Resource resource, + PropertySourceLoader loader, + List sourceList) { + if (resource.exists()) { + String name = "bladePropertySource: [" + location + "]"; + try { + sourceList.addAll(loader.load(name, resource)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + + @Override + public void afterPropertiesSet() throws Exception { + log.info("BladePropertySourcePostProcessor init."); + } + + @Override + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE; + } + + @Getter + @ToString + @EqualsAndHashCode + private static class PropertyFile implements Comparable { + private final int order; + private final String location; + private final String extension; + private final boolean loadActiveProfile; + + PropertyFile(int order, String location, boolean loadActiveProfile) { + this.order = order; + this.location = location; + this.loadActiveProfile = loadActiveProfile; + this.extension = Objects.requireNonNull(StringUtils.getFilenameExtension(location)); + } + + @Override + public int compareTo(PropertyFile other) { + return Integer.compare(this.order, other.order); + } + } +} diff --git a/blade-core-launch/src/main/java/org/springblade/core/launch/server/ServerInfo.java b/blade-core-launch/src/main/java/org/springblade/core/launch/server/ServerInfo.java new file mode 100644 index 0000000..d83e493 --- /dev/null +++ b/blade-core-launch/src/main/java/org/springblade/core/launch/server/ServerInfo.java @@ -0,0 +1,61 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.launch.server; + +import lombok.Getter; +import org.springblade.core.launch.utils.INetUtil; +import org.springframework.beans.factory.SmartInitializingSingleton; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.web.ServerProperties; + +/** + * 服务器信息 + * + * @author Chill + */ +@Getter +@AutoConfiguration +public class ServerInfo implements SmartInitializingSingleton { + private final ServerProperties serverProperties; + private String hostName; + private String ip; + private Integer port; + private String ipWithPort; + + @Autowired(required = false) + public ServerInfo(ServerProperties serverProperties) { + this.serverProperties = serverProperties; + } + + @Override + public void afterSingletonsInstantiated() { + this.hostName = INetUtil.getHostName(); + this.ip = INetUtil.getHostIp(); + this.port = serverProperties.getPort(); + this.ipWithPort = String.format("%s:%d", ip, port); + } +} diff --git a/blade-core-launch/src/main/java/org/springblade/core/launch/service/LauncherService.java b/blade-core-launch/src/main/java/org/springblade/core/launch/service/LauncherService.java new file mode 100644 index 0000000..d2b26dc --- /dev/null +++ b/blade-core-launch/src/main/java/org/springblade/core/launch/service/LauncherService.java @@ -0,0 +1,69 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.launch.service; + +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.core.Ordered; + +/** + * launcher 扩展 用于一些组件发现 + * + * @author Chill + */ +public interface LauncherService extends Ordered, Comparable { + + /** + * 启动时 处理 SpringApplicationBuilder + * + * @param builder SpringApplicationBuilder + * @param appName SpringApplicationAppName + * @param profile SpringApplicationProfile + * @param isLocalDev SpringApplicationIsLocalDev + */ + void launcher(SpringApplicationBuilder builder, String appName, String profile, boolean isLocalDev); + + /** + * 获取排列顺序 + * + * @return order + */ + @Override + default int getOrder() { + return 0; + } + + /** + * 对比排序 + * + * @param o LauncherService + * @return compare + */ + @Override + default int compareTo(LauncherService o) { + return Integer.compare(this.getOrder(), o.getOrder()); + } + +} diff --git a/blade-core-launch/src/main/java/org/springblade/core/launch/utils/INetUtil.java b/blade-core-launch/src/main/java/org/springblade/core/launch/utils/INetUtil.java new file mode 100644 index 0000000..fb98047 --- /dev/null +++ b/blade-core-launch/src/main/java/org/springblade/core/launch/utils/INetUtil.java @@ -0,0 +1,253 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.launch.utils; + +import org.springframework.util.ObjectUtils; + +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.ServerSocket; +import java.net.UnknownHostException; +import java.util.Enumeration; + +/** + * INet 相关工具 + * + * @author L.cm + */ +public class INetUtil { + public static final String LOCAL_HOST = "127.0.0.1"; + + /** + * 获取 服务器 hostname + * + * @return hostname + */ + public static String getHostName() { + String hostname; + try { + InetAddress address = InetAddress.getLocalHost(); + // force a best effort reverse DNS lookup + hostname = address.getHostName(); + if (ObjectUtils.isEmpty(hostname)) { + hostname = address.toString(); + } + } catch (UnknownHostException ignore) { + hostname = LOCAL_HOST; + } + return hostname; + } + + /** + * 获取 服务器 HostIp + * + * @return HostIp + */ + public static String getHostIp() { + String hostAddress; + try { + InetAddress address = INetUtil.getLocalHostLANAddress(); + // force a best effort reverse DNS lookup + hostAddress = address.getHostAddress(); + if (ObjectUtils.isEmpty(hostAddress)) { + hostAddress = address.toString(); + } + } catch (UnknownHostException ignore) { + hostAddress = LOCAL_HOST; + } + return hostAddress; + } + + /** + * https://stackoverflow.com/questions/9481865/getting-the-ip-address-of-the-current-machine-using-java + * + *

+ * Returns an InetAddress object encapsulating what is most likely the machine's LAN IP address. + *

+ * This method is intended for use as a replacement of JDK method InetAddress.getLocalHost, because + * that method is ambiguous on Linux systems. Linux systems enumerate the loopback network interface the same + * way as regular LAN network interfaces, but the JDK InetAddress.getLocalHost method does not + * specify the algorithm used to select the address returned under such circumstances, and will often return the + * loopback address, which is not valid for network communication. Details + * here. + *

+ * This method will scan all IP addresses on all network interfaces on the host machine to determine the IP address + * most likely to be the machine's LAN address. If the machine has multiple IP addresses, this method will prefer + * a site-local IP address (e.g. 192.168.x.x or 10.10.x.x, usually IPv4) if the machine has one (and will return the + * first site-local address if the machine has more than one), but if the machine does not hold a site-local + * address, this method will return simply the first non-loopback address found (IPv4 or IPv6). + *

+ * If this method cannot find a non-loopback address using this selection algorithm, it will fall back to + * calling and returning the result of JDK method InetAddress.getLocalHost. + *

+ * + * @throws UnknownHostException If the LAN address of the machine cannot be found. + */ + private static InetAddress getLocalHostLANAddress() throws UnknownHostException { + try { + InetAddress candidateAddress = null; + // Iterate all NICs (network interface cards)... + for (Enumeration ifaces = NetworkInterface.getNetworkInterfaces(); ifaces.hasMoreElements(); ) { + NetworkInterface iface = (NetworkInterface) ifaces.nextElement(); + // Iterate all IP addresses assigned to each card... + for (Enumeration inetAddrs = iface.getInetAddresses(); inetAddrs.hasMoreElements(); ) { + InetAddress inetAddr = (InetAddress) inetAddrs.nextElement(); + if (!inetAddr.isLoopbackAddress()) { + + if (inetAddr.isSiteLocalAddress()) { + // Found non-loopback site-local address. Return it immediately... + return inetAddr; + } else if (candidateAddress == null) { + // Found non-loopback address, but not necessarily site-local. + // Store it as a candidate to be returned if site-local address is not subsequently found... + candidateAddress = inetAddr; + // Note that we don't repeatedly assign non-loopback non-site-local addresses as candidates, + // only the first. For subsequent iterations, candidate will be non-null. + } + } + } + } + if (candidateAddress != null) { + // We did not find a site-local address, but we found some other non-loopback address. + // Server might have a non-site-local address assigned to its NIC (or it might be running + // IPv6 which deprecates the "site-local" concept). + // Return this non-loopback candidate address... + return candidateAddress; + } + // At this point, we did not find a non-loopback address. + // Fall back to returning whatever InetAddress.getLocalHost() returns... + InetAddress jdkSuppliedAddress = InetAddress.getLocalHost(); + if (jdkSuppliedAddress == null) { + throw new UnknownHostException("The JDK InetAddress.getLocalHost() method unexpectedly returned null."); + } + return jdkSuppliedAddress; + } catch (Exception e) { + UnknownHostException unknownHostException = new UnknownHostException("Failed to determine LAN address: " + e); + unknownHostException.initCause(e); + throw unknownHostException; + } + } + + /** + * 尝试端口时候被占用 + * + * @param port 端口号 + * @return 没有被占用:true,被占用:false + */ + public static boolean tryPort(int port) { + try (ServerSocket ignore = new ServerSocket(port)) { + return true; + } catch (Exception e) { + return false; + } + } + + /** + * 将 ip 转成 InetAddress + * + * @param ip ip + * @return InetAddress + */ + public static InetAddress getInetAddress(String ip) { + try { + return InetAddress.getByName(ip); + } catch (UnknownHostException e) { + return null; + } + } + + /** + * 判断是否内网 ip + * + * @param ip ip + * @return boolean + */ + public static boolean isInternalIp(String ip) { + return isInternalIp(getInetAddress(ip)); + } + + /** + * 判断是否内网 ip + * + * @param address InetAddress + * @return boolean + */ + public static boolean isInternalIp(InetAddress address) { + if (isLocalIp(address)) { + return true; + } + return isInternalIp(address.getAddress()); + } + + /** + * 判断是否本地 ip + * + * @param address InetAddress + * @return boolean + */ + public static boolean isLocalIp(InetAddress address) { + return address.isAnyLocalAddress() + || address.isLoopbackAddress() + || address.isSiteLocalAddress(); + } + + /** + * 判断是否内网 ip + * + * @param addr ip + * @return boolean + */ + public static boolean isInternalIp(byte[] addr) { + final byte b0 = addr[0]; + final byte b1 = addr[1]; + //10.x.x.x/8 + final byte section1 = 0x0A; + //172.16.x.x/12 + final byte section2 = (byte) 0xAC; + final byte section3 = (byte) 0x10; + final byte section4 = (byte) 0x1F; + //192.168.x.x/16 + final byte section5 = (byte) 0xC0; + final byte section6 = (byte) 0xA8; + switch (b0) { + case section1: + return true; + case section2: + if (b1 >= section3 && b1 <= section4) { + return true; + } + case section5: + if (b1 == section6) { + return true; + } + default: + return false; + } + } + + +} diff --git a/blade-core-launch/src/main/java/org/springblade/core/launch/utils/PropsUtil.java b/blade-core-launch/src/main/java/org/springblade/core/launch/utils/PropsUtil.java new file mode 100644 index 0000000..ec68c4b --- /dev/null +++ b/blade-core-launch/src/main/java/org/springblade/core/launch/utils/PropsUtil.java @@ -0,0 +1,52 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.launch.utils; + +import org.springframework.util.ObjectUtils; + +import java.util.Properties; + +/** + * 配置工具类 + * + * @author Chill + */ +public class PropsUtil { + + /** + * 设置配置值,已存在则跳过 + * + * @param props property + * @param key key + * @param value value + */ + public static void setProperty(Properties props, String key, String value) { + if (ObjectUtils.isEmpty(props.getProperty(key))) { + props.setProperty(key, value); + } + } + +} diff --git a/blade-core-log4j2/pom.xml b/blade-core-log4j2/pom.xml new file mode 100644 index 0000000..e9470e0 --- /dev/null +++ b/blade-core-log4j2/pom.xml @@ -0,0 +1,38 @@ + + + 4.0.0 + + BladeX-Tool + org.springblade + ${revision} + + + blade-core-log4j2 + ${project.artifactId} + ${project.parent.version} + jar + + + + org.springblade + blade-core-launch + + + org.springframework.boot + spring-boot-starter-log4j2 + + + com.lmax + disruptor + + + + org.springblade + blade-core-auto + provided + + + + diff --git a/blade-core-log4j2/src/main/java/org/springblade/core/log4j2/LogLauncherServiceImpl.java b/blade-core-log4j2/src/main/java/org/springblade/core/log4j2/LogLauncherServiceImpl.java new file mode 100644 index 0000000..03e039c --- /dev/null +++ b/blade-core-log4j2/src/main/java/org/springblade/core/log4j2/LogLauncherServiceImpl.java @@ -0,0 +1,54 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.log4j2; + +import org.springblade.core.auto.service.AutoService; +import org.springblade.core.launch.service.LauncherService; +import org.springframework.boot.builder.SpringApplicationBuilder; + +/** + * 日志启动器 + * + * @author L.cm + */ +@AutoService(LauncherService.class) +public class LogLauncherServiceImpl implements LauncherService { + + @Override + public void launcher(SpringApplicationBuilder builder, String appName, String profile, boolean isLocalDev) { + System.setProperty("logging.config", String.format("classpath:log/log4j2_%s.xml", profile)); + // RocketMQ-Client 4.2.0 Log4j2 配置文件冲突问题解决:https://www.jianshu.com/p/b30ae6dd3811 + System.setProperty("rocketmq.client.log.loadconfig", "false"); + // RocketMQ-Client 4.3 设置默认为 slf4j + System.setProperty("rocketmq.client.logUseSlf4j", "true"); + // 非本地 将 全部的 System.err 和 System.out 替换为log + if (!isLocalDev) { + System.setOut(LogPrintStream.out()); + System.setErr(LogPrintStream.err()); + } + } + +} diff --git a/blade-core-log4j2/src/main/java/org/springblade/core/log4j2/LogPrintStream.java b/blade-core-log4j2/src/main/java/org/springblade/core/log4j2/LogPrintStream.java new file mode 100644 index 0000000..e01ae2f --- /dev/null +++ b/blade-core-log4j2/src/main/java/org/springblade/core/log4j2/LogPrintStream.java @@ -0,0 +1,99 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.log4j2; + +import lombok.extern.slf4j.Slf4j; + +import java.io.PrintStream; +import java.util.Locale; + +/** + * 替换 系统 System.err 和 System.out 为log + * + * @author L.cm + */ +@Slf4j +public class LogPrintStream extends PrintStream { + private final boolean error; + + private LogPrintStream(boolean error) { + super(error ? System.err : System.out); + this.error = error; + } + + public static LogPrintStream out() { + return new LogPrintStream(false); + } + + public static LogPrintStream err() { + return new LogPrintStream(true); + } + + @Override + public void print(String s) { + if (error) { + log.error(s); + } else { + log.info(s); + } + } + + /** + * 重写掉它,因为它会打印很多无用的新行 + */ + @Override + public void println() { + } + + @Override + public void println(String x) { + if (error) { + log.error(x); + } else { + log.info(x); + } + } + + @Override + public PrintStream printf(String format, Object... args) { + if (error) { + log.error(String.format(format, args)); + } else { + log.info(String.format(format, args)); + } + return this; + } + + @Override + public PrintStream printf(Locale l, String format, Object... args) { + if (error) { + log.error(String.format(l, format, args)); + } else { + log.info(String.format(l, format, args)); + } + return this; + } +} diff --git a/blade-core-log4j2/src/main/resources/log/log4j2_appenders.xml b/blade-core-log4j2/src/main/resources/log/log4j2_appenders.xml new file mode 100644 index 0000000..2c248c7 --- /dev/null +++ b/blade-core-log4j2/src/main/resources/log/log4j2_appenders.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/blade-core-log4j2/src/main/resources/log/log4j2_dev.xml b/blade-core-log4j2/src/main/resources/log/log4j2_dev.xml new file mode 100644 index 0000000..13c6e48 --- /dev/null +++ b/blade-core-log4j2/src/main/resources/log/log4j2_dev.xml @@ -0,0 +1,32 @@ + + + + ${sys:spring.application.name} + ${env:LOG_BASE:-logs}/${appName} + ???? + %5p + %xwEx + yyyy-MM-dd HH:mm:ss.SSS + %clr{%d{${LOG_DATEFORMAT_PATTERN}}}{faint} %clr{${LOG_LEVEL_PATTERN}} %clr{${sys:PID}}{magenta} %clr{---}{faint} %clr{[%15.15t]}{faint} %clr{%C{36}.%M:%L}{cyan} %clr{:}{faint} %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD} + + + + + + + + + + + + + + + + + + + + + + diff --git a/blade-core-log4j2/src/main/resources/log/log4j2_ontest.xml b/blade-core-log4j2/src/main/resources/log/log4j2_ontest.xml new file mode 100644 index 0000000..aae80aa --- /dev/null +++ b/blade-core-log4j2/src/main/resources/log/log4j2_ontest.xml @@ -0,0 +1,31 @@ + + + + ${sys:spring.application.name} + ${env:LOG_BASE:-logs}/${appName} + ???? + %5p + %xwEx + yyyy-MM-dd HH:mm:ss.SSS + %clr{%d{${LOG_DATEFORMAT_PATTERN}}}{faint} %clr{${LOG_LEVEL_PATTERN}} %clr{${sys:PID}}{magenta} %clr{---}{faint} %clr{[%15.15t]}{faint} %clr{%-40.40c{1.}}{cyan} %clr{:}{faint} %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD} + + + + + + + + + + + + + + + + + + + + + diff --git a/blade-core-log4j2/src/main/resources/log/log4j2_prod.xml b/blade-core-log4j2/src/main/resources/log/log4j2_prod.xml new file mode 100644 index 0000000..18151f5 --- /dev/null +++ b/blade-core-log4j2/src/main/resources/log/log4j2_prod.xml @@ -0,0 +1,29 @@ + + + + ${sys:spring.application.name} + ${env:LOG_BASE:-logs}/${appName} + ???? + %5p + %xwEx + yyyy-MM-dd HH:mm:ss.SSS + %clr{%d{${LOG_DATEFORMAT_PATTERN}}}{faint} %clr{${LOG_LEVEL_PATTERN}} %clr{${sys:PID}}{magenta} %clr{---}{faint} %clr{[%15.15t]}{faint} %clr{%-40.40c{1.}}{cyan} %clr{:}{faint} %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD} + + + + + + + + + + + + + + + + + + + diff --git a/blade-core-log4j2/src/main/resources/log/log4j2_test.xml b/blade-core-log4j2/src/main/resources/log/log4j2_test.xml new file mode 100644 index 0000000..d077d19 --- /dev/null +++ b/blade-core-log4j2/src/main/resources/log/log4j2_test.xml @@ -0,0 +1,30 @@ + + + + ${sys:spring.application.name} + ${env:LOG_BASE:-logs}/${appName} + ???? + %5p + %xwEx + yyyy-MM-dd HH:mm:ss.SSS + %clr{%d{${LOG_DATEFORMAT_PATTERN}}}{faint} %clr{${LOG_LEVEL_PATTERN}} %clr{${sys:PID}}{magenta} %clr{---}{faint} %clr{[%15.15t]}{faint} %clr{%C{36}.%M:%L}{cyan} %clr{:}{faint} %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD} + + + + + + + + + + + + + + + + + + + + diff --git a/blade-core-oauth2/pom.xml b/blade-core-oauth2/pom.xml new file mode 100644 index 0000000..6696e0b --- /dev/null +++ b/blade-core-oauth2/pom.xml @@ -0,0 +1,54 @@ + + + 4.0.0 + + BladeX-Tool + org.springblade + ${revision} + + + blade-core-oauth2 + ${project.artifactId} + ${project.parent.version} + jar + + + + + org.springblade + blade-core-secure + + + org.springblade + blade-starter-social + + + org.springblade + blade-starter-redis + + + + com.github.whvcse + easy-captcha + + + + org.springframework.session + spring-session-data-redis + + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + + org.springblade + blade-core-auto + provided + + + + diff --git a/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/config/OAuth2AutoConfiguration.java b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/config/OAuth2AutoConfiguration.java new file mode 100644 index 0000000..a17d14e --- /dev/null +++ b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/config/OAuth2AutoConfiguration.java @@ -0,0 +1,94 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oauth2.config; + +import lombok.AllArgsConstructor; +import org.springblade.core.jwt.props.JwtProperties; +import org.springblade.core.oauth2.granter.TokenGranter; +import org.springblade.core.oauth2.granter.TokenGranterEnhancer; +import org.springblade.core.oauth2.granter.TokenGranterFactory; +import org.springblade.core.oauth2.handler.*; +import org.springblade.core.oauth2.props.OAuth2Properties; +import org.springblade.core.oauth2.service.OAuth2ClientService; +import org.springblade.core.oauth2.service.OAuth2UserService; +import org.springblade.core.oauth2.service.impl.OAuth2ClientDetailService; +import org.springblade.core.oauth2.service.impl.OAuth2UserDetailService; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.core.JdbcTemplate; + +import java.util.List; + +/** + * OAuth2Configuration + * + * @author BladeX + */ +@AllArgsConstructor +@Configuration(proxyBeanMethods = false) +@EnableConfigurationProperties(OAuth2Properties.class) +@ConditionalOnProperty(value = OAuth2Properties.PREFIX + ".enabled", havingValue = "true", matchIfMissing = true) +public class OAuth2AutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public AuthorizationHandler authorizationHandler() { + return new OAuth2AuthorizationHandler(); + } + + @Bean + @ConditionalOnMissingBean + public PasswordHandler passwordHandler() { + return new OAuth2PasswordHandler(); + } + + @Bean + @ConditionalOnMissingBean + public TokenHandler tokenHandler(JwtProperties properties) { + return new OAuth2TokenHandler(properties); + } + + @Bean + @ConditionalOnMissingBean + public OAuth2ClientService oAuth2ClientService(JdbcTemplate jdbcTemplate) { + return new OAuth2ClientDetailService(jdbcTemplate); + } + + @Bean + @ConditionalOnMissingBean + public OAuth2UserService oAuth2UserService(JdbcTemplate jdbcTemplate) { + return new OAuth2UserDetailService(jdbcTemplate); + } + + @Bean + public TokenGranterFactory tokenGranterFactory(List tokenGranters, List tokenGranterEnhancers, OAuth2Properties properties) { + return new TokenGranterFactory(tokenGranters, tokenGranterEnhancers, properties); + } + +} diff --git a/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/config/OAuth2WebConfiguration.java b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/config/OAuth2WebConfiguration.java new file mode 100644 index 0000000..34973ee --- /dev/null +++ b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/config/OAuth2WebConfiguration.java @@ -0,0 +1,46 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oauth2.config; + + +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +/** + * OAuth资源配置 + * + * @author BladeX + */ +@AutoConfiguration +public class OAuth2WebConfiguration implements WebMvcConfigurer { + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/"); + } + +} diff --git a/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/constant/OAuth2AuthorizationConstant.java b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/constant/OAuth2AuthorizationConstant.java new file mode 100644 index 0000000..b3acaaa --- /dev/null +++ b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/constant/OAuth2AuthorizationConstant.java @@ -0,0 +1,79 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oauth2.constant; + +/** + * OAuth2AuthorizationConstant + * + * @author BladeX + */ +public interface OAuth2AuthorizationConstant { + + /** + * 用户session key + */ + String AUTHORIZATION_SESSION_KEY = "user"; + + /** + * 授权请求key + */ + String AUTHORIZATION_REQUEST_KEY = "authorizationRequest"; + + /** + * 跳转参数 + */ + String REDIRECT_URL = "redirect:"; + + /** + * 授权地址 + */ + String AUTHORIZE_URL = "/oauth/authorize"; + + /** + * 登录地址 + */ + String LOGIN_URL = "/oauth/login"; + + /** + * 错误地址 + */ + String ERROR_URL = "/oauth/error"; + + /** + * 授权视图 + */ + String AUTHORIZE_MODEL = "authorize"; + + /** + * 登录视图 + */ + String LOGIN_MODEL = "login"; + + /** + * 错误视图 + */ + String ERROR_MODEL = "error"; +} diff --git a/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/constant/OAuth2ClientConstant.java b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/constant/OAuth2ClientConstant.java new file mode 100644 index 0000000..3094e90 --- /dev/null +++ b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/constant/OAuth2ClientConstant.java @@ -0,0 +1,54 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oauth2.constant; + +/** + * OAuth2ClientConstant + * + * @author BladeX + */ +public interface OAuth2ClientConstant { + + /** + * blade_client表字段 + */ + String CLIENT_FIELDS = "id, client_id, client_secret, resource_ids, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove"; + + /** + * blade_client查询语句 + */ + String BASE_STATEMENT = "select " + CLIENT_FIELDS + " from blade_client"; + + /** + * blade_client查询排序 + */ + String DEFAULT_FIND_STATEMENT = BASE_STATEMENT + " order by client_id"; + + /** + * 查询client_id + */ + String DEFAULT_SELECT_STATEMENT = BASE_STATEMENT + " where client_id = ?"; +} diff --git a/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/constant/OAuth2GranterConstant.java b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/constant/OAuth2GranterConstant.java new file mode 100644 index 0000000..14641b0 --- /dev/null +++ b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/constant/OAuth2GranterConstant.java @@ -0,0 +1,76 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oauth2.constant; + +/** + * GranterTypeConstant + * + * @author BladeX + */ +public interface OAuth2GranterConstant { + + /** + * 授权码模式 + */ + String AUTHORIZATION_CODE = "authorization_code"; + /** + * 密码模式 + */ + String PASSWORD = "password"; + /** + * 刷新token模式 + */ + String REFRESH_TOKEN = "refresh_token"; + /** + * 客户端模式 + */ + String CLIENT_CREDENTIALS = "client_credentials"; + /** + * 简化模式 + */ + String IMPLICIT = "implicit"; + /** + * 验证码模式 + */ + String CAPTCHA = "captcha"; + /** + * 手机验证码模式 + */ + String SMS_CODE = "sms_code"; + /** + * 微信小程序模式 + */ + String WECHAT_APPLET = "wechat_applet"; + /** + * 开放平台模式 + */ + String SOCIAL = "social"; + /** + * 注册模式 + */ + String REGISTER = "register"; + +} diff --git a/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/constant/OAuth2ParameterConstant.java b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/constant/OAuth2ParameterConstant.java new file mode 100644 index 0000000..71635e4 --- /dev/null +++ b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/constant/OAuth2ParameterConstant.java @@ -0,0 +1,103 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oauth2.constant; + +/** + * OAuth2常量. + * + * @author BladeX + */ +public interface OAuth2ParameterConstant { + + /** + * 客户端id + */ + String CLIENT_ID = "client_id"; + /** + * 客户端密钥 + */ + String CLIENT_SECRET = "client_secret"; + /** + * 令牌 + */ + String ACCESS_TOKEN = "access_token"; + /** + * 刷新令牌 + */ + String REFRESH_TOKEN = "refresh_token"; + /** + * 租户编号 + */ + String TENANT_ID = "tenant_id"; + /** + * 用户名字 + */ + String NAME = "name"; + /** + * 用户名 + */ + String USERNAME = "username"; + /** + * 密码 + */ + String PASSWORD = "password"; + /** + * 手机号 + */ + String PHONE = "phone"; + /** + * 电子游戏 + */ + String EMAIL = "email"; + /** + * 授权类型 + */ + String GRANT_TYPE = "grant_type"; + /** + * 响应类型 + */ + String SCOPE = "scope"; + /** + * 重定向地址 + */ + String REDIRECT_URI = "redirect_uri"; + /** + * 返回类型 + */ + String RESPONSE_TYPE = "response_type"; + /** + * 状态 + */ + String STATE = "state"; + /** + * 验证 + */ + String CODE = "code"; + /** + * 来源 + */ + String SOURCE = "source"; +} diff --git a/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/constant/OAuth2ResponseConstant.java b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/constant/OAuth2ResponseConstant.java new file mode 100644 index 0000000..4407ee7 --- /dev/null +++ b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/constant/OAuth2ResponseConstant.java @@ -0,0 +1,37 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oauth2.constant; + +/** + * OAuth2ResponseConstant + * + * @author BladeX + */ +public interface OAuth2ResponseConstant { + String SUCCESS = "success"; + String ERROR_CODE = "error_code"; + String ERROR_DESCRIPTION = "error_description"; +} diff --git a/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/constant/OAuth2TokenConstant.java b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/constant/OAuth2TokenConstant.java new file mode 100644 index 0000000..0b2c182 --- /dev/null +++ b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/constant/OAuth2TokenConstant.java @@ -0,0 +1,57 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oauth2.constant; + +/** + * TokenConstant + * + * @author BladeX + */ +public interface OAuth2TokenConstant { + + String HEADER_AUTHORIZATION = "Authorization"; + String HEADER_AUTHORIZATION_PREFIX = "Basic "; + String TOKEN_HEADER = "Blade-Auth"; + String TENANT_HEADER = "Tenant-Id"; + String DEFAULT_TENANT_ID = "000000"; + String USER_HEADER = "User-Id"; + String DEPT_HEADER = "Dept-Id"; + String ROLE_HEADER = "Role-Id"; + String USER_TYPE_HEADER = "User-Type"; + String DEFAULT_USER_TYPE = "web"; + String USER_FAIL_KEY = "blade:user::blade:fail:"; + String CAPTCHA_CACHE_KEY = "blade:auth::blade:captcha:"; + String CAPTCHA_HEADER_KEY = "Captcha-Key"; + String CAPTCHA_HEADER_CODE = "Captcha-Code"; + String CAPTCHA_NOT_CORRECT = "验证码不正确"; + String TOKEN_NOT_PERMISSION = "令牌授权已过期"; + String USER_NOT_FOUND = "用户名或密码错误"; + String USER_HAS_NO_ROLE = "未获得用户的角色信息"; + String USER_HAS_NO_TENANT = "未获得用户的租户信息"; + String USER_HAS_NO_TENANT_PERMISSION = "租户授权已过期,请联系管理员"; + String USER_HAS_TOO_MANY_FAILS = "登录错误次数过多,请稍后再试"; + String DEFAULT_AVATAR = "https://bladex.cn/images/logo.png"; +} diff --git a/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/constant/OAuth2UserConstant.java b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/constant/OAuth2UserConstant.java new file mode 100644 index 0000000..bfbe7b7 --- /dev/null +++ b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/constant/OAuth2UserConstant.java @@ -0,0 +1,45 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oauth2.constant; + +/** + * OAuth2UserConstant + * + * @author BladeX + */ +public interface OAuth2UserConstant { + + /** + * blade_user查询 + */ + String DEFAULT_USERID_SELECT_STATEMENT = "select id as user_id, tenant_id , account, password from blade_user where id = ?"; + + /** + * blade_user查询 + */ + String DEFAULT_USERNAME_SELECT_STATEMENT = "select id as user_id, tenant_id , account, password from blade_user where account = ?"; + +} diff --git a/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/endpoint/OAuth2AuthorizationEndpoint.java b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/endpoint/OAuth2AuthorizationEndpoint.java new file mode 100644 index 0000000..31e2d86 --- /dev/null +++ b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/endpoint/OAuth2AuthorizationEndpoint.java @@ -0,0 +1,266 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oauth2.endpoint; + +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpSession; +import lombok.RequiredArgsConstructor; +import org.springblade.core.oauth2.constant.OAuth2AuthorizationConstant; +import org.springblade.core.oauth2.handler.PasswordHandler; +import org.springblade.core.oauth2.provider.OAuth2AuthorizationRequest; +import org.springblade.core.oauth2.provider.OAuth2Request; +import org.springblade.core.oauth2.service.OAuth2Client; +import org.springblade.core.oauth2.service.OAuth2ClientService; +import org.springblade.core.oauth2.service.OAuth2User; +import org.springblade.core.oauth2.service.OAuth2UserService; +import org.springblade.core.oauth2.utils.OAuth2CodeUtil; +import org.springblade.core.redis.cache.BladeRedis; +import org.springblade.core.tool.utils.Func; +import org.springblade.core.tool.utils.StringUtil; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.SessionAttribute; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; + +import java.time.Duration; +import java.util.Optional; + +import static org.springblade.core.oauth2.constant.OAuth2ParameterConstant.*; + +/** + * AuthorizationEndpoint + * + * @author BladeX + */ +@Controller +@RequiredArgsConstructor +@Tag(name = "用户授权码模式认证", description = "2 - OAuth2授权码模式端点") +public class OAuth2AuthorizationEndpoint implements OAuth2AuthorizationConstant { + + private final BladeRedis bladeRedis; + + private final OAuth2ClientService clientService; + + private final OAuth2UserService userService; + + private final PasswordHandler passwordHandler; + + @GetMapping("/oauth/login") + public String loginPage(HttpSession session, Model model) { + // 从session中获取授权请求参数 + Optional.ofNullable((OAuth2AuthorizationRequest) session.getAttribute(AUTHORIZATION_REQUEST_KEY)) + .ifPresent(authorizationRequest -> model.addAttribute(AUTHORIZATION_REQUEST_KEY, authorizationRequest)); + // 返回登录页面视图 + return LOGIN_MODEL; + } + + + @PostMapping("/oauth/login/perform") + public String performLogin(@SessionAttribute(AUTHORIZATION_REQUEST_KEY) OAuth2AuthorizationRequest authorizationRequest, + RedirectAttributes redirectAttributes, HttpSession session) { + // 根据用户名和密码验证用户 + return Optional.ofNullable(authenticateUser(session, authorizationRequest)) + .map(user -> { + // 用户验证成功,处理授权请求参数和重定向 + authorizationRequest.setTenantId(user.getTenantId()); + session.setAttribute(AUTHORIZATION_REQUEST_KEY, authorizationRequest); + redirectAttributes.addAllAttributes(authorizationRequest.getParameters()); + return REDIRECT_URL + AUTHORIZE_URL; // 重定向回授权视图 + }) + .orElse(REDIRECT_URL + ERROR_URL); // 用户验证失败,重定向回失败视图 + } + + @GetMapping("/oauth/authorize") + public String authorize(@SessionAttribute(value = AUTHORIZATION_REQUEST_KEY, required = false) OAuth2AuthorizationRequest authorizationRequest, + HttpSession session, Model model) { + // 获取授权请求参数 + OAuth2AuthorizationRequest request = OAuth2AuthorizationRequest.create().buildParameters(); + + // 设置请求参数 + Optional.ofNullable(authorizationRequest).ifPresentOrElse(authReq -> { + if (request.getState() == null) { + request.setState(authReq.getState()); + } + if (request.getClientId() != null) { + session.setAttribute(AUTHORIZATION_REQUEST_KEY, request); + } + }, () -> session.setAttribute(AUTHORIZATION_REQUEST_KEY, request)); + + // 获取用户信息并跳转 + return Optional.ofNullable(session.getAttribute(AUTHORIZATION_SESSION_KEY)) + .map(obj -> (OAuth2User) obj) + .map(user -> { + model.addAttribute(USERNAME, user.getAccount()); + model.addAllAttributes(request.getParameters()); + return AUTHORIZE_MODEL; // 用户已登录,显示授权页面 + }) + .orElse(REDIRECT_URL + LOGIN_URL); // 用户未登录,重定向到登录页面 + } + + @PostMapping("/oauth/authorize/perform") + public String performAuthorize(@RequestParam Boolean approval, + @RequestParam(required = false) String state, + @SessionAttribute(AUTHORIZATION_REQUEST_KEY) OAuth2AuthorizationRequest authorizationRequest, + RedirectAttributes redirectAttributes, HttpSession session) { + // 此处可以添加用户同意授权的处理逻辑 + if (!approval) { + // 用户拒绝授权,返回授权页面 + return REDIRECT_URL + LOGIN_URL; + } + + // 获取客户端信息 + OAuth2Client client = clientService.loadByClientId(authorizationRequest.getClientId()); + + // 校验回调地址信息 + if (!clientService.validateRedirectUri(client, authorizationRequest.getRedirectUri())) { + // 重定向URI参数不匹配,返回错误页面 + return ERROR_MODEL; + } + + // 生成授权码 + String code = createCode(); + + // 设置用户信息 + OAuth2User user = (OAuth2User) session.getAttribute(AUTHORIZATION_SESSION_KEY); + if (user == null) { + // 用户未登录,重定向到登录页面 + return REDIRECT_URL + LOGIN_URL; + } + + // 校验state参数 + if (Func.equalsSafe(authorizationRequest.getState(), state)) { + // 保存授权码 + saveCode(code, user); + } else { + // 重定向URI和state参数不匹配,返回错误页面 + return ERROR_MODEL; + } + + // 使用RedirectAttributes添加授权码和state参数 + redirectAttributes.addAttribute(CODE, code); + + // 添加tenantId参数为state参数 + if (authorizationRequest.getTenantId() != null) { + redirectAttributes.addAttribute(STATE, authorizationRequest.getTenantId()); + } + // 用户自定义state参数则覆盖 + if (state != null) { + redirectAttributes.addAttribute(STATE, state); + } + + // 重定向到客户端提供的重定向URI + return REDIRECT_URL + authorizationRequest.getRedirectUri(); + } + + @GetMapping("/oauth/authorize/logout") + public String logout(HttpSession session) { + // 退出登录,清除session中的用户信息 + session.removeAttribute(AUTHORIZATION_SESSION_KEY); + return REDIRECT_URL + LOGIN_URL; + } + + @GetMapping("/oauth/error") + public String error() { + // 返回错误页面 + return ERROR_MODEL; + } + + private OAuth2User authenticateUser(HttpSession session, OAuth2AuthorizationRequest authorizationRequest) { + // 创建 OAuth2 请求对象并构建参数 + OAuth2Request request = OAuth2Request.create().buildArgs(); + + // 获取请求参数 + String username = request.getUsername(); + String password = request.getPassword(); + String clientId = authorizationRequest.getClientId(); + String redirectUri = authorizationRequest.getRedirectUri(); + + // 获取客户端信息 + OAuth2Client client = clientService.loadByClientId(clientId); + + // 校验回调地址信息 + if (!clientService.validateRedirectUri(client, redirectUri)) { + return null; + } + + // 获取用户信息 + OAuth2User user = userService.loadByUsername(username, request); + + // 校验用户信息 + if (!userService.validateUser(user)) { + return null; + } + + // 校验用户密码 + if (!passwordHandler.matches(password, user.getPassword())) { + return null; + } + + // 将用户信息存入session + session.setAttribute(AUTHORIZATION_SESSION_KEY, user); + + // 返回用户信息 + return user; + } + + /** + * 授权码模式获取授权码 + * + * @return 授权码 + */ + private String createCode() { + // 生成6位随机数作为授权码 + String code = StringUtil.random(6); + if (bladeRedis.exists(OAuth2CodeUtil.codeKey(code))) { + // 如果生成的授权码已存在,则递归调用重新生成 + return createCode(); + } + return code; + } + + /** + * 保存code信息 + */ + private void saveCode(String code, OAuth2User user) { + // 保存code信息到redis,30分钟过期 + bladeRedis.setEx(OAuth2CodeUtil.codeKey(code), user, Duration.ofMinutes(30)); + } + + /** + * 根据code获取用户信息 + * + * @param code code + * @return 用户信息 + */ + public OAuth2User getUserByCode(String code) { + // 根据code从redis中获取用户信息 + return bladeRedis.get(OAuth2CodeUtil.codeKey(code)); + } + +} diff --git a/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/endpoint/OAuth2SocialEndpoint.java b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/endpoint/OAuth2SocialEndpoint.java new file mode 100644 index 0000000..875b595 --- /dev/null +++ b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/endpoint/OAuth2SocialEndpoint.java @@ -0,0 +1,102 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oauth2.endpoint; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletResponse; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import me.zhyd.oauth.model.AuthCallback; +import me.zhyd.oauth.model.AuthToken; +import me.zhyd.oauth.request.AuthRequest; +import me.zhyd.oauth.utils.AuthStateUtils; +import org.springblade.core.social.props.SocialProperties; +import org.springblade.core.social.utils.SocialUtil; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.io.IOException; + +/** + * 第三方登录端点 + * + * @author BladeX + */ +@Slf4j +@RestController +@AllArgsConstructor +@ConditionalOnProperty(value = "social.enabled", havingValue = "true") +@Tag(name = "开放平台登录", description = "3 - 开放平台登录端点") +public class OAuth2SocialEndpoint { + + private final SocialProperties socialProperties; + + /** + * 授权完毕跳转 + */ + @Operation(summary = "授权完毕跳转") + @RequestMapping("/oauth/render/{source}") + public void renderAuth(@PathVariable("source") String source, HttpServletResponse response) throws IOException { + AuthRequest authRequest = SocialUtil.getAuthRequest(source, socialProperties); + String authorizeUrl = authRequest.authorize(AuthStateUtils.createState()); + response.sendRedirect(authorizeUrl); + } + + /** + * 获取认证信息 + */ + @Operation(summary = "获取认证信息") + @RequestMapping("/oauth/callback/{source}") + public Object login(@PathVariable("source") String source, AuthCallback callback) { + AuthRequest authRequest = SocialUtil.getAuthRequest(source, socialProperties); + return authRequest.login(callback); + } + + /** + * 撤销授权 + */ + @Operation(summary = "撤销授权") + @RequestMapping("/oauth/revoke/{source}/{token}") + public Object revokeAuth(@PathVariable("source") String source, @PathVariable("token") String token) { + AuthRequest authRequest = SocialUtil.getAuthRequest(source, socialProperties); + return authRequest.revoke(AuthToken.builder().accessToken(token).build()); + } + + /** + * 续期accessToken + */ + @Operation(summary = "续期令牌") + @RequestMapping("/oauth/refresh/{source}") + public Object refreshAuth(@PathVariable("source") String source, String token) { + AuthRequest authRequest = SocialUtil.getAuthRequest(source, socialProperties); + return authRequest.refresh(AuthToken.builder().refreshToken(token).build()); + } + + +} diff --git a/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/endpoint/OAuth2TokenEndPoint.java b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/endpoint/OAuth2TokenEndPoint.java new file mode 100644 index 0000000..ea5e71d --- /dev/null +++ b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/endpoint/OAuth2TokenEndPoint.java @@ -0,0 +1,170 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oauth2.endpoint; + +import com.wf.captcha.SpecCaptcha; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.AllArgsConstructor; +import org.springblade.core.cache.utils.CacheUtil; +import org.springblade.core.jwt.JwtUtil; +import org.springblade.core.jwt.props.JwtProperties; +import org.springblade.core.oauth2.constant.OAuth2ParameterConstant; +import org.springblade.core.oauth2.granter.TokenGranter; +import org.springblade.core.oauth2.granter.TokenGranterFactory; +import org.springblade.core.oauth2.handler.AuthorizationHandler; +import org.springblade.core.oauth2.handler.TokenHandler; +import org.springblade.core.oauth2.provider.OAuth2Request; +import org.springblade.core.oauth2.provider.OAuth2Token; +import org.springblade.core.oauth2.provider.OAuth2Validation; +import org.springblade.core.oauth2.service.OAuth2User; +import org.springblade.core.oauth2.utils.OAuth2ExceptionUtil; +import org.springblade.core.redis.cache.BladeRedis; +import org.springblade.core.secure.BladeUser; +import org.springblade.core.secure.utils.AuthUtil; +import org.springblade.core.tool.support.Kv; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.time.Duration; +import java.util.UUID; + +import static org.springblade.core.cache.constant.CacheConstant.*; +import static org.springblade.core.oauth2.constant.OAuth2TokenConstant.CAPTCHA_CACHE_KEY; + +/** + * OAuth2认证端点 + * + * @author BladeX + */ +@RestController +@AllArgsConstructor +@Tag(name = "用户授权认证", description = "1 - OAuth2授权认证端点") +public class OAuth2TokenEndPoint { + + private final BladeRedis bladeRedis; + private final JwtProperties jwtProperties; + + private final TokenGranterFactory granterFactory; + private final AuthorizationHandler authorizationHandler; + private final TokenHandler tokenHandler; + + + @PostMapping("/oauth/token") + @Operation( + summary = "获取Token", + description = "OAuth2认证接口", + parameters = { + @Parameter(in = ParameterIn.QUERY, name = OAuth2ParameterConstant.USERNAME, description = "账号", schema = @Schema(type = "string")), + @Parameter(in = ParameterIn.QUERY, name = OAuth2ParameterConstant.PASSWORD, description = "密码", schema = @Schema(type = "string")), + @Parameter(in = ParameterIn.QUERY, name = OAuth2ParameterConstant.GRANT_TYPE, description = "授权类型", schema = @Schema(type = "string")), + @Parameter(in = ParameterIn.QUERY, name = OAuth2ParameterConstant.REFRESH_TOKEN, description = "刷新token", schema = @Schema(type = "string")), + @Parameter(in = ParameterIn.QUERY, name = OAuth2ParameterConstant.SCOPE, description = "权限范围", schema = @Schema(type = "string")) + } + ) + public ResponseEntity token() { + // 创建 OAuth2 请求对象并构建参数 + OAuth2Request request = OAuth2Request.create().buildArgs(); + + // 根据请求的授权类型创建对应的 TokenGranter + TokenGranter tokenGranter = granterFactory.create(request.getGrantType()); + + // 使用 TokenGranter 获取用户信息 + OAuth2User user = tokenGranter.user(request); + + // 使用授权处理器对用户进行验证 + OAuth2Validation validation = authorizationHandler.authValidation(user, request); + + // 检查验证是否成功 + if (!validation.isSuccess()) { + // 验证失败处理逻辑 + authorizationHandler.authFailure(user, request, validation); + + // 根据验证失败的错误代码抛出异常 + OAuth2ExceptionUtil.throwFromCode(validation.getCode()); + } + + // 创建令牌 + OAuth2Token token = tokenGranter.token(user, request); + + // 对令牌进行增强处理 + OAuth2Token enhanceToken = tokenHandler.enhance(user, token, request); + + // 验证成功处理逻辑 + authorizationHandler.authSuccessful(user, request); + + // 返回增强后的令牌 + return ResponseEntity.ok(enhanceToken.getArgs()); + } + + + @GetMapping("/oauth/logout") + @Operation(summary = "退出登录") + public Kv logout() { + BladeUser user = AuthUtil.getUser(); + if (user != null && jwtProperties.getState()) { + OAuth2Request request = OAuth2Request.create().buildHeaderArgs(); + String token = JwtUtil.getToken(request.getToken()); + JwtUtil.removeAccessToken(user.getTenantId(), user.getClientId(), String.valueOf(user.getUserId()), token); + } + return Kv.create().set("success", "true").set("msg", "success"); + } + + @GetMapping("/oauth/captcha") + @Operation(summary = "获取验证码") + public Kv captcha() { + SpecCaptcha specCaptcha = new SpecCaptcha(130, 48, 5); + String verCode = specCaptcha.text().toLowerCase(); + String key = UUID.randomUUID().toString(); + // 存入redis并设置过期时间为30分钟 + bladeRedis.setEx(CAPTCHA_CACHE_KEY + key, verCode, Duration.ofMinutes(30)); + // 将key和base64返回给前端 + return Kv.create().set("key", key).set("image", specCaptcha.toBase64()); + } + + @GetMapping("/oauth/clear-cache") + @Operation(summary = "清除缓存") + public Kv clearCache() { + CacheUtil.clear(BIZ_CACHE); + CacheUtil.clear(USER_CACHE); + CacheUtil.clear(DICT_CACHE); + CacheUtil.clear(FLOW_CACHE); + CacheUtil.clear(SYS_CACHE); + CacheUtil.clear(PARAM_CACHE); + CacheUtil.clear(RESOURCE_CACHE); + CacheUtil.clear(MENU_CACHE); + CacheUtil.clear(DICT_CACHE, Boolean.FALSE); + CacheUtil.clear(MENU_CACHE, Boolean.FALSE); + CacheUtil.clear(SYS_CACHE, Boolean.FALSE); + CacheUtil.clear(PARAM_CACHE, Boolean.FALSE); + return Kv.create().set("success", "true").set("msg", "success"); + } +} diff --git a/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/exception/ClientInvalidException.java b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/exception/ClientInvalidException.java new file mode 100644 index 0000000..f80b006 --- /dev/null +++ b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/exception/ClientInvalidException.java @@ -0,0 +1,43 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oauth2.exception; + +/** + * 客户端认证失败 + * + * @author BladeX + */ +public class ClientInvalidException extends OAuth2Exception { + + public ClientInvalidException(String msg) { + super(ExceptionCode.INVALID_CLIENT, msg); + } + + public ClientInvalidException(String msg, Throwable cause) { + super(ExceptionCode.INVALID_CLIENT, msg, cause); + } + +} diff --git a/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/exception/ClientNotFoundException.java b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/exception/ClientNotFoundException.java new file mode 100644 index 0000000..7ac9f65 --- /dev/null +++ b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/exception/ClientNotFoundException.java @@ -0,0 +1,43 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oauth2.exception; + +/** + * 客户端未找到 + * + * @author BladeX + */ +public class ClientNotFoundException extends OAuth2Exception { + + public ClientNotFoundException(String msg) { + super(ExceptionCode.CLIENT_NOT_FOUND, msg); + } + + public ClientNotFoundException(String msg, Throwable cause) { + super(ExceptionCode.CLIENT_NOT_FOUND, msg, cause); + } + +} diff --git a/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/exception/ClientUnauthorizedException.java b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/exception/ClientUnauthorizedException.java new file mode 100644 index 0000000..4b84b34 --- /dev/null +++ b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/exception/ClientUnauthorizedException.java @@ -0,0 +1,43 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oauth2.exception; + +/** + * 客户端未授权 + * + * @author BladeX + */ +public class ClientUnauthorizedException extends OAuth2Exception { + + public ClientUnauthorizedException(String msg) { + super(ExceptionCode.UNAUTHORIZED_CLIENT, msg); + } + + public ClientUnauthorizedException(String msg, Throwable cause) { + super(ExceptionCode.UNAUTHORIZED_CLIENT, msg, cause); + } + +} diff --git a/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/exception/ExceptionCode.java b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/exception/ExceptionCode.java new file mode 100644 index 0000000..0682104 --- /dev/null +++ b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/exception/ExceptionCode.java @@ -0,0 +1,150 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oauth2.exception; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import static org.springblade.core.oauth2.exception.OAuth2ErrorMessage.INVALID_ERROR_CODE; + +/** + * OAuth2ExceptionCode + * + * @author BladeX + */ +@Getter +@AllArgsConstructor +public enum ExceptionCode { + + /** + * 无效请求 - 请求缺少必要的参数或格式不正确。 + */ + INVALID_REQUEST(OAuth2ErrorCode.INVALID_REQUEST, "无效请求"), + + /** + * 用户不存在 - 指定的用户ID不存在或无效。 + */ + USER_NOT_FOUND(OAuth2ErrorCode.USER_NOT_FOUND, "用户不存在"), + + /** + * 用户租户不存在 - 指定的用户租户未授权。 + */ + USER_TENANT_NOT_FOUND(OAuth2ErrorCode.USER_TENANT_NOT_FOUND, "用户租户不存在"), + + /** + * 用户登录失败次数过多 - 用户登录失败次数过多。 + */ + USER_TOO_MANY_FAILS(OAuth2ErrorCode.USER_TOO_MANY_FAILS, "用户登录失败次数过多"), + + /** + * 用户认证失败 - 指定的用户认证信息错误或无效。 + */ + INVALID_USER(OAuth2ErrorCode.INVALID_USER, "认证信息错误或无效"), + + /** + * 用户未授权 - 指定的用户未授权。 + */ + UNAUTHORIZED_USER(OAuth2ErrorCode.UNAUTHORIZED_USER, "认证信息错误或无效"), + + /** + * 用户租户未授权 - 指定的用户租户未授权。 + */ + UNAUTHORIZED_USER_TENANT(OAuth2ErrorCode.UNAUTHORIZED_USER_TENANT, "用户租户未授权"), + + /** + * 用户未授权 - 指定的用户未授权。 + */ + INVALID_REFRESH_TOKEN(OAuth2ErrorCode.INVALID_REFRESH_TOKEN, "令牌刷新错误或无效"), + + /** + * 客户端不存在 - 指定的客户端ID不存在或无效。 + */ + CLIENT_NOT_FOUND(OAuth2ErrorCode.CLIENT_NOT_FOUND, "客户端不存在"), + + /** + * 客户端认证失败 - 客户端提供的认证信息错误或无效。 + */ + INVALID_CLIENT(OAuth2ErrorCode.INVALID_CLIENT, "客户端认证失败"), + + /** + * 回调地址错误或无效 - 客户端回调地址错误或无效。 + */ + INVALID_CLIENT_REDIRECT_URI(OAuth2ErrorCode.INVALID_CLIENT_REDIRECT_URI, "客户端未授权"), + + /** + * 客户端未授权 - 客户端无权执行此操作。 + */ + UNAUTHORIZED_CLIENT(OAuth2ErrorCode.UNAUTHORIZED_CLIENT, "客户端未授权"), + + /** + * 不支持的授权类型 - 请求的授权类型不被服务器支持。 + */ + UNSUPPORTED_GRANT_TYPE(OAuth2ErrorCode.UNSUPPORTED_GRANT_TYPE, "不支持的授权类型"), + + /** + * 无效的授权类型 - 提供的授权令牌无效、过期或被撤销。 + */ + INVALID_GRANTER(OAuth2ErrorCode.INVALID_GRANTER, "无效的授权类型"), + + /** + * 无效的无效的授权范围 - 请求的无效的授权范围无效、未知或格式不正确。 + */ + INVALID_SCOPE(OAuth2ErrorCode.INVALID_SCOPE, "授权范围"), + + /** + * 服务器错误 - 服务器内部错误,无法完成请求。 + */ + SERVER_ERROR(OAuth2ErrorCode.SERVER_ERROR, "服务器错误"), + + /** + * 访问被拒绝 - 由于各种原因,服务器拒绝执行此操作。 + */ + ACCESS_DENIED(OAuth2ErrorCode.ACCESS_DENIED, "访问被拒绝"), + + /** + * 服务暂不可用 - 服务器暂时过载或维护,无法处理请求。 + */ + TEMPORARILY_UNAVAILABLE(OAuth2ErrorCode.TEMPORARILY_UNAVAILABLE, "服务暂不可用"); + + final int code; + final String message; + + /** + * 通过错误代码获取枚举 + * + * @param code 错误代码 + * @return ExceptionCodeEnum + */ + public static ExceptionCode of(int code) { + for (ExceptionCode value : ExceptionCode.values()) { + if (value.code == code) { + return value; + } + } + throw new IllegalArgumentException(String.format(INVALID_ERROR_CODE, code)); + } +} + diff --git a/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/exception/GranterInvalidException.java b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/exception/GranterInvalidException.java new file mode 100644 index 0000000..4b0d5a0 --- /dev/null +++ b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/exception/GranterInvalidException.java @@ -0,0 +1,43 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oauth2.exception; + +/** + * 无效的授权 + * + * @author BladeX + */ +public class GranterInvalidException extends OAuth2Exception { + + public GranterInvalidException(String msg) { + super(ExceptionCode.INVALID_GRANTER, msg); + } + + public GranterInvalidException(String msg, Throwable cause) { + super(ExceptionCode.INVALID_GRANTER, msg, cause); + } + +} diff --git a/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/exception/OAuth2ErrorCode.java b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/exception/OAuth2ErrorCode.java new file mode 100644 index 0000000..5e1d796 --- /dev/null +++ b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/exception/OAuth2ErrorCode.java @@ -0,0 +1,106 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oauth2.exception; + +/** + * OAuth2ErrorCodes + * + * @author BladeX + */ +public interface OAuth2ErrorCode { + /** + * 无效请求 - 请求缺少必要的参数或格式不正确。 + */ + int INVALID_REQUEST = 2000; + /** + * 用户不存在 - 指定的用户ID不存在或无效。 + */ + int USER_NOT_FOUND = 2001; + /** + * 用户租户不存在 - 指定的用户租户未授权。 + */ + int USER_TENANT_NOT_FOUND = 2002; + /** + * 用户登录失败次数过多 - 用户登录失败次数过多。 + */ + int USER_TOO_MANY_FAILS = 2003; + /** + * 用户认证失败 - 指定的用户认证信息错误或无效。 + */ + int INVALID_USER = 2004; + /** + * 用户未授权 - 指定的用户未授权。 + */ + int UNAUTHORIZED_USER = 2005; + /** + * 用户租户未授权 - 指定的用户租户未授权。 + */ + int UNAUTHORIZED_USER_TENANT = 2006; + /** + * 令牌刷新错误或无效 - 刷新令牌认证信息错误或无效。 + */ + int INVALID_REFRESH_TOKEN = 2010; + /** + * 客户端不存在 - 指定的客户端ID不存在或无效。 + */ + int CLIENT_NOT_FOUND = 3000; + /** + * 客户端认证失败 - 客户端提供的认证信息错误或无效。 + */ + int INVALID_CLIENT = 3001; + /** + * 回调地址错误或无效 - 客户端回调地址错误或无效。 + */ + int INVALID_CLIENT_REDIRECT_URI = 3002; + /** + * 客户端未授权 - 客户端无权执行此操作。 + */ + int UNAUTHORIZED_CLIENT = 3003; + /** + * 不支持的授权类型 - 请求的授权类型不被服务器支持。 + */ + int UNSUPPORTED_GRANT_TYPE = 4000; + /** + * 无效的授权类型 - 提供的授权类型无效、过期或被撤销。 + */ + int INVALID_GRANTER = 4001; + /** + * 无效的授权范围 - 请求的授权范围无效、未知或格式不正确。 + */ + int INVALID_SCOPE = 4002; + /** + * 服务器错误 - 服务器内部错误,无法完成请求。 + */ + int SERVER_ERROR = 5000; + /** + * 访问被拒绝 - 由于各种原因,服务器拒绝执行此操作。 + */ + int ACCESS_DENIED = 5001; + /** + * 服务暂不可用 - 服务器暂时过载或维护,无法处理请求。 + */ + int TEMPORARILY_UNAVAILABLE = 5002; +} diff --git a/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/exception/OAuth2ErrorMessage.java b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/exception/OAuth2ErrorMessage.java new file mode 100644 index 0000000..e519341 --- /dev/null +++ b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/exception/OAuth2ErrorMessage.java @@ -0,0 +1,47 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oauth2.exception; + +/** + * OAuth2ErrorMessage + * + * @author BladeX + */ +public interface OAuth2ErrorMessage { + String INVALID_GRANT_TYPE = "无效的授权类型: %s"; + + String CLIENT_AUTHORIZATION_FAILED = "客户端认证失败, 请检查请求头 [Authorization] 信息"; + + String CLIENT_TOKEN_PARSE_FAILED = "客户端令牌解析失败"; + + String INVALID_CLIENT_TOKEN = "客户端令牌不合法"; + + String AUTHORIZATION_NOT_FOUND = "请求头中未找到 [Authorization] 信息"; + String INVALID_ERROR_CODE = "无效的错误代码: %s"; + String USER_HAS_NO_TENANT = "未获得用户的租户信息"; + String USER_HAS_NO_TENANT_PERMISSION = "租户授权已过期,请联系管理员"; + String USER_HAS_TOO_MANY_FAILS = "登录错误次数过多,请稍后再试"; +} diff --git a/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/exception/OAuth2Exception.java b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/exception/OAuth2Exception.java new file mode 100644 index 0000000..60efdf9 --- /dev/null +++ b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/exception/OAuth2Exception.java @@ -0,0 +1,75 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oauth2.exception; + +import lombok.Getter; + +import java.io.Serial; + +/** + * OAuth2通用异常 + * + * @author BladeX + */ +@Getter +public class OAuth2Exception extends RuntimeException { + + @Serial + private static final long serialVersionUID = 1L; + + private final ExceptionCode exceptionCode; + + public OAuth2Exception(String message) { + super(message); + this.exceptionCode = ExceptionCode.ACCESS_DENIED; + } + + public OAuth2Exception(ExceptionCode exceptionCode) { + super(exceptionCode.getMessage()); + this.exceptionCode = exceptionCode; + } + + public OAuth2Exception(ExceptionCode exceptionCode, String message) { + super(message); + this.exceptionCode = exceptionCode; + } + + public OAuth2Exception(ExceptionCode exceptionCode, Throwable cause) { + super(cause); + this.exceptionCode = exceptionCode; + } + + public OAuth2Exception(ExceptionCode exceptionCode, String message, Throwable cause) { + super(message, cause); + this.exceptionCode = exceptionCode; + } + + @Override + public Throwable fillInStackTrace() { + return this; + } + +} diff --git a/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/exception/OAuth2ExceptionHandler.java b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/exception/OAuth2ExceptionHandler.java new file mode 100644 index 0000000..dd84429 --- /dev/null +++ b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/exception/OAuth2ExceptionHandler.java @@ -0,0 +1,62 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oauth2.exception; + +import io.jsonwebtoken.JwtException; +import org.springblade.core.oauth2.endpoint.OAuth2AuthorizationEndpoint; +import org.springblade.core.oauth2.endpoint.OAuth2SocialEndpoint; +import org.springblade.core.oauth2.endpoint.OAuth2TokenEndPoint; +import org.springblade.core.oauth2.provider.OAuth2Response; +import org.springblade.core.secure.exception.SecureException; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +/** + * OAuth2ExceptionHandler + * + * @author BladeX + */ +@ControllerAdvice(basePackageClasses = {OAuth2AuthorizationEndpoint.class, OAuth2SocialEndpoint.class, OAuth2TokenEndPoint.class}) +public class OAuth2ExceptionHandler { + @ExceptionHandler(OAuth2Exception.class) + public ResponseEntity handleOAuth2Exception(OAuth2Exception ex) { + // 统一处理验证失败的情况 + return ResponseEntity.ok(OAuth2Response.create().of(Boolean.FALSE, ex.getExceptionCode().getCode(), ex.getMessage())); + } + + @ExceptionHandler(SecureException.class) + public ResponseEntity handleSecureException(SecureException ex) { + // 统一处理验证失败的情况 + return ResponseEntity.ok(OAuth2Response.create().of(Boolean.FALSE, OAuth2ErrorCode.INVALID_USER, ex.getMessage())); + } + + @ExceptionHandler(JwtException.class) + public ResponseEntity handleJwtException(JwtException ex) { + // 统一处理验证失败的情况 + return ResponseEntity.ok(OAuth2Response.create().of(Boolean.FALSE, OAuth2ErrorCode.ACCESS_DENIED, ex.getMessage())); + } +} diff --git a/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/exception/UserInvalidException.java b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/exception/UserInvalidException.java new file mode 100644 index 0000000..a7cf66e --- /dev/null +++ b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/exception/UserInvalidException.java @@ -0,0 +1,43 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oauth2.exception; + +/** + * 用户认证失败 + * + * @author BladeX + */ +public class UserInvalidException extends OAuth2Exception { + + public UserInvalidException(String msg) { + super(ExceptionCode.INVALID_USER, msg); + } + + public UserInvalidException(String msg, Throwable cause) { + super(ExceptionCode.INVALID_USER, msg, cause); + } + +} diff --git a/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/exception/UserUnauthorizedException.java b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/exception/UserUnauthorizedException.java new file mode 100644 index 0000000..c0c4a24 --- /dev/null +++ b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/exception/UserUnauthorizedException.java @@ -0,0 +1,43 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oauth2.exception; + +/** + * 用户未授权 + * + * @author BladeX + */ +public class UserUnauthorizedException extends OAuth2Exception { + + public UserUnauthorizedException(String msg) { + super(ExceptionCode.UNAUTHORIZED_USER, msg); + } + + public UserUnauthorizedException(String msg, Throwable cause) { + super(ExceptionCode.UNAUTHORIZED_USER, msg, cause); + } + +} diff --git a/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/exception/UsernameNotFoundException.java b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/exception/UsernameNotFoundException.java new file mode 100644 index 0000000..176d536 --- /dev/null +++ b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/exception/UsernameNotFoundException.java @@ -0,0 +1,43 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oauth2.exception; + +/** + * 用户名未找到 + * + * @author BladeX + */ +public class UsernameNotFoundException extends OAuth2Exception { + + public UsernameNotFoundException(String msg) { + super(ExceptionCode.USER_NOT_FOUND, msg); + } + + public UsernameNotFoundException(String msg, Throwable cause) { + super(ExceptionCode.USER_NOT_FOUND, msg, cause); + } + +} diff --git a/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/granter/AbstractTokenGranter.java b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/granter/AbstractTokenGranter.java new file mode 100644 index 0000000..d385ab4 --- /dev/null +++ b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/granter/AbstractTokenGranter.java @@ -0,0 +1,148 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oauth2.granter; + +import lombok.RequiredArgsConstructor; +import org.springblade.core.launch.constant.TokenConstant; +import org.springblade.core.oauth2.exception.OAuth2ErrorCode; +import org.springblade.core.oauth2.handler.PasswordHandler; +import org.springblade.core.oauth2.provider.OAuth2Request; +import org.springblade.core.oauth2.provider.OAuth2Token; +import org.springblade.core.oauth2.service.OAuth2Client; +import org.springblade.core.oauth2.service.OAuth2ClientService; +import org.springblade.core.oauth2.service.OAuth2User; +import org.springblade.core.oauth2.service.OAuth2UserService; +import org.springblade.core.oauth2.utils.OAuth2ExceptionUtil; +import org.springblade.core.oauth2.utils.OAuth2Util; +import org.springblade.core.secure.TokenInfo; +import org.springblade.core.tool.utils.Func; +import org.springblade.core.tool.utils.StringUtil; + +/** + * AbstractTokenGranter + * + * @author BladeX + */ +@RequiredArgsConstructor +public abstract class AbstractTokenGranter implements TokenGranter { + + private final OAuth2ClientService clientService; + private final OAuth2UserService userService; + private final PasswordHandler passwordHandler; + + protected TokenGranterEnhancer enhancer; + + public abstract String type(); + + @Override + public void enhancer(TokenGranterEnhancer enhancer) { + this.enhancer = enhancer; + } + + @Override + public OAuth2Client client(OAuth2Request request) { + // 解析请求头 + String[] tokens = request.getClientFromAuthorization(); + // 获取clientId + String clientId = tokens[0]; + // 获取clientSecret + String clientSecret = tokens[1]; + + // 获取客户端信息 + OAuth2Client client = clientService.loadByClientId(clientId); + + // 校验客户端信息 + if (!clientService.validateClient(client, clientId, clientSecret)) { + OAuth2ExceptionUtil.throwFromCode(OAuth2ErrorCode.INVALID_CLIENT); + } + + // 校验授权类型 + if (!clientService.validateGranter(client, type())) { + OAuth2ExceptionUtil.throwFromCode(OAuth2ErrorCode.INVALID_GRANTER); + } + + // 返回客户端信息 + return client; + } + + @Override + public OAuth2User user(OAuth2Request request) { + // 获取用户信息 + OAuth2User user = (StringUtil.isNotBlank(request.getUserId()) && request.isRefreshToken()) + ? userService.loadByUserId(request.getUserId(), request) + : userService.loadByUsername(request.getUsername(), request); + + // 校验用户信息 + if (!userService.validateUser(user)) { + OAuth2ExceptionUtil.throwFromCode(OAuth2ErrorCode.INVALID_USER); + } + + // 校验用户密码 + if ((request.isCaptchaCode() || request.isPassword()) && !passwordHandler.matches(request.getPassword(), user.getPassword())) { + OAuth2ExceptionUtil.throwFromCode(OAuth2ErrorCode.INVALID_USER); + } + + // 设置客户端信息 + user.setClient(client(request)); + + // 返回用户信息 + return user; + } + + @Override + public OAuth2Token token(OAuth2User user, OAuth2Request request) { + TokenInfo accessToken = OAuth2Util.createAccessToken(user); + TokenInfo refreshToken = OAuth2Util.createRefreshToken(user); + + OAuth2Token token = OAuth2Token.create(); + + token.getArgs().set(TokenConstant.TENANT_ID, user.getTenantId()) + .set(TokenConstant.USER_ID, user.getUserId()) + .set(TokenConstant.DEPT_ID, user.getDeptId()) + .set(TokenConstant.POST_ID, user.getPostId()) + .set(TokenConstant.ROLE_ID, user.getRoleId()) + .set(TokenConstant.OAUTH_ID, user.getOauthId()) + .set(TokenConstant.ACCOUNT, user.getAccount()) + .set(TokenConstant.USER_NAME, user.getAccount()) + .set(TokenConstant.NICK_NAME, user.getName()) + .set(TokenConstant.REAL_NAME, user.getRealName()) + .set(TokenConstant.ROLE_NAME, Func.join(user.getAuthorities())) + .set(TokenConstant.AVATAR, Func.toStr(user.getAvatar(), TokenConstant.DEFAULT_AVATAR)) + .set(TokenConstant.ACCESS_TOKEN, accessToken.getToken()) + .set(TokenConstant.REFRESH_TOKEN, refreshToken.getToken()) + .set(TokenConstant.TOKEN_TYPE, TokenConstant.BEARER) + .set(TokenConstant.EXPIRES_IN, accessToken.getExpire()) + .set(TokenConstant.DETAIL, user.getDetail()) + .set(TokenConstant.LICENSE, TokenConstant.LICENSE_NAME); + + return token.setAccessToken(accessToken.getToken()) + .setAccessTokenExpire(accessToken.getExpire()) + .setRefreshToken(refreshToken.getToken()) + .setRefreshTokenExpire(refreshToken.getExpire()); + } + + +} diff --git a/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/granter/AuthorizationCodeGranter.java b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/granter/AuthorizationCodeGranter.java new file mode 100644 index 0000000..46af962 --- /dev/null +++ b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/granter/AuthorizationCodeGranter.java @@ -0,0 +1,100 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oauth2.granter; + +import org.springblade.core.oauth2.exception.ExceptionCode; +import org.springblade.core.oauth2.exception.OAuth2ErrorCode; +import org.springblade.core.oauth2.exception.UserInvalidException; +import org.springblade.core.oauth2.handler.PasswordHandler; +import org.springblade.core.oauth2.provider.OAuth2Request; +import org.springblade.core.oauth2.service.OAuth2Client; +import org.springblade.core.oauth2.service.OAuth2ClientService; +import org.springblade.core.oauth2.service.OAuth2User; +import org.springblade.core.oauth2.service.OAuth2UserService; +import org.springblade.core.oauth2.utils.OAuth2CodeUtil; +import org.springblade.core.oauth2.utils.OAuth2ExceptionUtil; +import org.springblade.core.redis.cache.BladeRedis; +import org.springblade.core.tool.utils.ObjectUtil; +import org.springblade.core.tool.utils.StringUtil; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +/** + * AuthorizationCodeGranter + * + * @author BladeX + */ +@Component +public class AuthorizationCodeGranter extends AbstractTokenGranter { + + private final OAuth2UserService userService; + private final PasswordHandler passwordHandler; + private final BladeRedis bladeRedis; + + public AuthorizationCodeGranter(OAuth2ClientService clientService, OAuth2UserService userService, PasswordHandler passwordHandler, BladeRedis bladeRedis) { + super(clientService, userService, passwordHandler); + this.userService = userService; + this.passwordHandler = passwordHandler; + this.bladeRedis = bladeRedis; + } + + @Override + public String type() { + return AUTHORIZATION_CODE; + } + + @Override + public OAuth2User user(OAuth2Request request) { + // 获取客户端信息并校验 + OAuth2Client client = client(request); + if (!StringUtil.equals(client.getWebServerRedirectUri(), request.getRedirectUri())) { + OAuth2ExceptionUtil.throwFromCode(OAuth2ErrorCode.INVALID_CLIENT_REDIRECT_URI); + } + // 根据code获取用户信息 + String code = request.getCode(); + OAuth2User user = bladeRedis.get(OAuth2CodeUtil.codeKey(code)); + // 判断用户是否存在 + if (ObjectUtil.isNotEmpty(user)) { + // 校验用户信息 + if (!userService.validateUser(user)) { + OAuth2ExceptionUtil.throwFromCode(OAuth2ErrorCode.INVALID_USER); + } + // 校验用户密码 + if ((request.isCaptchaCode() || request.isPassword()) && !passwordHandler.matches(request.getPassword(), user.getPassword())) { + OAuth2ExceptionUtil.throwFromCode(OAuth2ErrorCode.INVALID_USER); + } + // 设置客户端信息 + user.setClient(client); + // 返回user + return Optional.ofNullable(this.enhancer) + .map(enhancer -> enhancer.enhance(user, request)) + .orElse(user); + } + throw new UserInvalidException(ExceptionCode.INVALID_USER.getMessage()); + } + +} diff --git a/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/granter/ClientCredentialsGranter.java b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/granter/ClientCredentialsGranter.java new file mode 100644 index 0000000..90ec286 --- /dev/null +++ b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/granter/ClientCredentialsGranter.java @@ -0,0 +1,90 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oauth2.granter; + +import org.springblade.core.launch.constant.TokenConstant; +import org.springblade.core.oauth2.handler.PasswordHandler; +import org.springblade.core.oauth2.provider.OAuth2Request; +import org.springblade.core.oauth2.provider.OAuth2Token; +import org.springblade.core.oauth2.service.OAuth2Client; +import org.springblade.core.oauth2.service.OAuth2ClientService; +import org.springblade.core.oauth2.service.OAuth2User; +import org.springblade.core.oauth2.service.OAuth2UserService; +import org.springblade.core.oauth2.service.impl.OAuth2UserDetail; +import org.springblade.core.oauth2.utils.OAuth2Util; +import org.springblade.core.secure.TokenInfo; +import org.springblade.core.tool.utils.Func; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +/** + * ClientCredentialsGranter + * + * @author BladeX + */ +@Component +public class ClientCredentialsGranter extends AbstractTokenGranter { + + public ClientCredentialsGranter(OAuth2ClientService clientService, OAuth2UserService userService, PasswordHandler passwordHandler) { + super(clientService, userService, passwordHandler); + } + + @Override + public String type() { + return CLIENT_CREDENTIALS; + } + + @Override + public OAuth2User user(OAuth2Request request) { + OAuth2UserDetail user = new OAuth2UserDetail(); + OAuth2Client client = client(request); + user.setClient(client); + user.setAccount(client.getClientId()); + user.setName(client.getClientId()); + return Optional.ofNullable(this.enhancer) + .map(enhancer -> enhancer.enhance(user, request)) + .orElse(user); + } + + @Override + public OAuth2Token token(OAuth2User user, OAuth2Request request) { + TokenInfo accessToken = OAuth2Util.createClientAccessToken(user.getClient()); + + OAuth2Token token = OAuth2Token.create(); + + token.getArgs().set(TokenConstant.CLIENT_ID, user.getClient().getClientId()) + .set(TokenConstant.AVATAR, Func.toStr(user.getAvatar(), TokenConstant.DEFAULT_AVATAR)) + .set(TokenConstant.ACCESS_TOKEN, accessToken.getToken()) + .set(TokenConstant.TOKEN_TYPE, TokenConstant.BEARER) + .set(TokenConstant.EXPIRES_IN, accessToken.getExpire()) + .set(TokenConstant.DETAIL, user.getDetail()) + .set(TokenConstant.LICENSE, TokenConstant.LICENSE_NAME); + + return token.setAccessToken(accessToken.getToken()).setAccessTokenExpire(accessToken.getExpire()); + } + +} diff --git a/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/granter/ImplicitGranter.java b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/granter/ImplicitGranter.java new file mode 100644 index 0000000..eda710c --- /dev/null +++ b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/granter/ImplicitGranter.java @@ -0,0 +1,94 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oauth2.granter; + +import org.springblade.core.launch.constant.TokenConstant; +import org.springblade.core.oauth2.handler.PasswordHandler; +import org.springblade.core.oauth2.provider.OAuth2Request; +import org.springblade.core.oauth2.provider.OAuth2Token; +import org.springblade.core.oauth2.service.OAuth2Client; +import org.springblade.core.oauth2.service.OAuth2ClientService; +import org.springblade.core.oauth2.service.OAuth2User; +import org.springblade.core.oauth2.service.OAuth2UserService; +import org.springblade.core.oauth2.service.impl.OAuth2ClientDetail; +import org.springblade.core.oauth2.service.impl.OAuth2UserDetail; +import org.springblade.core.oauth2.utils.OAuth2Util; +import org.springblade.core.secure.TokenInfo; +import org.springblade.core.tool.utils.Func; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +/** + * ImplicitGranter + * + * @author BladeX + */ +@Component +public class ImplicitGranter extends AbstractTokenGranter { + + public ImplicitGranter(OAuth2ClientService clientService, OAuth2UserService userService, PasswordHandler passwordHandler) { + super(clientService, userService, passwordHandler); + } + + @Override + public String type() { + return IMPLICIT; + } + + @Override + public OAuth2Client client(OAuth2Request request) { + return new OAuth2ClientDetail(); + } + + @Override + public OAuth2User user(OAuth2Request request) { + OAuth2UserDetail user = new OAuth2UserDetail(); + user.setAccount(request.getUsername()); + user.setName(request.getUsername()); + return Optional.ofNullable(this.enhancer) + .map(enhancer -> enhancer.enhance(user, request)) + .orElse(user); + } + + @Override + public OAuth2Token token(OAuth2User user, OAuth2Request request) { + TokenInfo accessToken = OAuth2Util.createImplicitAccessToken(user); + + OAuth2Token token = OAuth2Token.create(); + + token.getArgs().set(TokenConstant.USER_NAME, user.getAccount()) + .set(TokenConstant.AVATAR, Func.toStr(user.getAvatar(), TokenConstant.DEFAULT_AVATAR)) + .set(TokenConstant.ACCESS_TOKEN, accessToken.getToken()) + .set(TokenConstant.TOKEN_TYPE, TokenConstant.BEARER) + .set(TokenConstant.EXPIRES_IN, accessToken.getExpire()) + .set(TokenConstant.DETAIL, user.getDetail()) + .set(TokenConstant.LICENSE, TokenConstant.LICENSE_NAME); + + return token.setAccessToken(accessToken.getToken()).setAccessTokenExpire(accessToken.getExpire()); + } + +} diff --git a/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/granter/PasswordTokenGranter.java b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/granter/PasswordTokenGranter.java new file mode 100644 index 0000000..b08562d --- /dev/null +++ b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/granter/PasswordTokenGranter.java @@ -0,0 +1,62 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oauth2.granter; + +import org.springblade.core.oauth2.handler.PasswordHandler; +import org.springblade.core.oauth2.provider.OAuth2Request; +import org.springblade.core.oauth2.service.OAuth2ClientService; +import org.springblade.core.oauth2.service.OAuth2User; +import org.springblade.core.oauth2.service.OAuth2UserService; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +/** + * PasswordTokenGranter + * + * @author BladeX + */ +@Component +public class PasswordTokenGranter extends AbstractTokenGranter { + + public PasswordTokenGranter(OAuth2ClientService clientService, OAuth2UserService userService, PasswordHandler passwordHandler) { + super(clientService, userService, passwordHandler); + } + + @Override + public String type() { + return PASSWORD; + } + + @Override + public OAuth2User user(OAuth2Request request) { + OAuth2User user = super.user(request); + return Optional.ofNullable(this.enhancer) + .map(enhancer -> enhancer.enhance(user, request)) + .orElse(user); + } + +} diff --git a/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/granter/RefreshTokenGranter.java b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/granter/RefreshTokenGranter.java new file mode 100644 index 0000000..6f3090a --- /dev/null +++ b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/granter/RefreshTokenGranter.java @@ -0,0 +1,118 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oauth2.granter; + +import io.jsonwebtoken.Claims; +import org.springblade.core.jwt.JwtUtil; +import org.springblade.core.jwt.props.JwtProperties; +import org.springblade.core.launch.constant.TokenConstant; +import org.springblade.core.oauth2.exception.OAuth2ErrorCode; +import org.springblade.core.oauth2.handler.PasswordHandler; +import org.springblade.core.oauth2.provider.OAuth2Request; +import org.springblade.core.oauth2.service.OAuth2ClientService; +import org.springblade.core.oauth2.service.OAuth2User; +import org.springblade.core.oauth2.service.OAuth2UserService; +import org.springblade.core.oauth2.utils.OAuth2ExceptionUtil; +import org.springblade.core.tool.utils.StringUtil; +import org.springframework.stereotype.Component; + +import java.util.Objects; +import java.util.Optional; + +/** + * PasswordTokenGranter + * + * @author BladeX + */ +@Component +public class RefreshTokenGranter extends PasswordTokenGranter { + + private final JwtProperties jwtProperties; + + public RefreshTokenGranter(OAuth2ClientService clientService, OAuth2UserService userService, PasswordHandler passwordHandler, JwtProperties jwtProperties) { + super(clientService, userService, passwordHandler); + this.jwtProperties = jwtProperties; + } + + @Override + public String type() { + return REFRESH_TOKEN; + } + + @Override + public OAuth2User user(OAuth2Request request) { + String refreshToken = request.getRefreshToken(); + Claims refreshClaims = Objects.requireNonNull(JwtUtil.parseJWT(refreshToken)); + // 校验refreshToken的合法性 + if (!judgeRefreshToken(refreshClaims, refreshToken)) { + OAuth2ExceptionUtil.throwFromCode(OAuth2ErrorCode.INVALID_REFRESH_TOKEN); + } + // 校验refreshToken的格式 + String tokenType = String.valueOf(refreshClaims.get(TokenConstant.TOKEN_TYPE)); + if (!StringUtil.equals(tokenType, TokenConstant.REFRESH_TOKEN)) { + OAuth2ExceptionUtil.throwFromCode(OAuth2ErrorCode.INVALID_REFRESH_TOKEN); + } + // 校验refreshToken是否可获取username + String userId = String.valueOf(refreshClaims.get(TokenConstant.USER_ID)); + if (StringUtil.isBlank(userId)) { + OAuth2ExceptionUtil.throwFromCode(OAuth2ErrorCode.USER_NOT_FOUND); + } + // 设置username + request.setUserId(userId); + return super.user(request); + } + + + /** + * 校验refreshToken的合法性 + * + * @param refreshToken 待校验的refreshToken + * @return refreshToken是否合法 + */ + private boolean judgeRefreshToken(Claims refreshClaims, String refreshToken) { + // 首先检查JWT是否启用单人登录模式,如果不是则直接返回true + if (!jwtProperties.getState() || !jwtProperties.getSingle()) { + return true; + } + + // 解析JWT,如果无法解析则认为不合法 + return Optional.ofNullable(refreshClaims) + .map(claims -> { + // 从JWT claims中提取tenantId, clientId, 和 userId + String tenantId = String.valueOf(claims.get(TokenConstant.TENANT_ID)); + String clientId = String.valueOf(claims.get(TokenConstant.CLIENT_ID)); + String userId = String.valueOf(claims.get(TokenConstant.USER_ID)); + + // 根据提取的信息和refreshToken生成新的token + String token = JwtUtil.getRefreshToken(tenantId, clientId, userId, refreshToken); + + // 比较新生成的token与传入的refreshToken是否一致,如果一致则认为合法 + return StringUtil.equalsIgnoreCase(token, refreshToken); + }) + .orElse(false); // 如果claims为空,则返回false + } + +} diff --git a/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/granter/TokenGranter.java b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/granter/TokenGranter.java new file mode 100644 index 0000000..1156ad6 --- /dev/null +++ b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/granter/TokenGranter.java @@ -0,0 +1,80 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oauth2.granter; + +import org.springblade.core.oauth2.constant.OAuth2GranterConstant; +import org.springblade.core.oauth2.provider.OAuth2Request; +import org.springblade.core.oauth2.provider.OAuth2Token; +import org.springblade.core.oauth2.service.OAuth2Client; +import org.springblade.core.oauth2.service.OAuth2User; + +/** + * 授权认证统一接口. + * + * @author BladeX + */ +public interface TokenGranter extends OAuth2GranterConstant { + + /** + * 获取授权模式 + * + * @return String + */ + String type(); + + /** + * 获取客户端信息 + * + * @param request 授权参数 + * @return OAuth2Client + */ + OAuth2Client client(OAuth2Request request); + + /** + * 获取用户信息 + * + * @param request 授权参数 + * @return OAuth2User + */ + OAuth2User user(OAuth2Request request); + + /** + * 创建令牌 + * + * @param user 用户信息 + * @param request 授权参数 + * @return OAuth2Token + */ + OAuth2Token token(OAuth2User user, OAuth2Request request); + + /** + * 自定义增强 + * + * @param enhancer enhancer + */ + void enhancer(TokenGranterEnhancer enhancer); + +} diff --git a/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/granter/TokenGranterEnhancer.java b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/granter/TokenGranterEnhancer.java new file mode 100644 index 0000000..68bff68 --- /dev/null +++ b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/granter/TokenGranterEnhancer.java @@ -0,0 +1,55 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oauth2.granter; + +import org.springblade.core.oauth2.constant.OAuth2GranterConstant; +import org.springblade.core.oauth2.provider.OAuth2Request; +import org.springblade.core.oauth2.service.OAuth2User; + +/** + * TokenGranterEnhancer + * + * @author BladeX + */ +public interface TokenGranterEnhancer extends OAuth2GranterConstant { + + /** + * 获取授权模式 + * + * @return String + */ + String type(); + + /** + * 增强用户令牌 + * + * @param user 用户信息 + * @param request 授权参数 + * @return OAuth2User + */ + OAuth2User enhance(OAuth2User user, OAuth2Request request); + +} diff --git a/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/granter/TokenGranterFactory.java b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/granter/TokenGranterFactory.java new file mode 100644 index 0000000..52a228b --- /dev/null +++ b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/granter/TokenGranterFactory.java @@ -0,0 +1,120 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oauth2.granter; + +import lombok.AllArgsConstructor; +import org.springblade.core.oauth2.exception.GranterInvalidException; +import org.springblade.core.oauth2.props.OAuth2Properties; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import static org.springblade.core.oauth2.constant.OAuth2GranterConstant.*; +import static org.springblade.core.oauth2.exception.OAuth2ErrorMessage.INVALID_GRANT_TYPE; + +/** + * TokenGranterFactory + * + * @author BladeX + */ +@AllArgsConstructor +public class TokenGranterFactory { + + /** + * TokenGranter 集合 + */ + private final List tokenGranters; + + /** + * TokenGranterEnhancer 集合 + */ + private final List tokenGranterEnhancers; + + /** + * OAuth2 属性配置 + */ + private final OAuth2Properties properties; + + /** + * TokenGranter 缓存池,使用 ConcurrentHashMap 保证线程安全。 + */ + private static final Map GRANTER_POOL = new ConcurrentHashMap<>(); + + + /** + * 根据授权模式获取 TokenGranter,如果缓存中不存在,则尝试创建。 + * + * @param grantType 授权模式 + * @return TokenGranter 实例 + * @throws IllegalArgumentException 如果请求的授权类型不支持 + */ + public TokenGranter create(String grantType) { + // 使用 computeIfAbsent 实现延迟加载 + return GRANTER_POOL.computeIfAbsent(grantType, this::initializeTokenGranter); + } + + /** + * 尝试根据授权类型初始化 TokenGranter。 + * + * @param grantType 授权模式 + * @return 初始化的 TokenGranter + * @throws GranterInvalidException 如果无法识别授权类型 + */ + private TokenGranter initializeTokenGranter(String grantType) { + // 根据授权类型查找对应的 TokenGranter 并应用第一个找到的增强类 + return tokenGranters.stream() + .filter(granter -> granter.type().equals(grantType) && isGranterEnabled(granter)) + .peek(granter -> tokenGranterEnhancers.stream() + .filter(enhancer -> enhancer.type().equals(grantType)) + .findFirst() + .ifPresent(granter::enhancer)) + .findFirst() + .orElseThrow(() -> new GranterInvalidException(String.format(INVALID_GRANT_TYPE, grantType))); + } + + /** + * 判断 TokenGranter 是否启用。 + * + * @param granter TokenGranter 实例 + * @return 是否启用 + */ + private boolean isGranterEnabled(TokenGranter granter) { + return switch (granter.type()) { + case AUTHORIZATION_CODE -> properties.getGranter().getAuthorizationCode(); + case PASSWORD -> properties.getGranter().getPassword(); + case REFRESH_TOKEN -> properties.getGranter().getRefreshToken(); + case CLIENT_CREDENTIALS -> properties.getGranter().getClientCredentials(); + case IMPLICIT -> properties.getGranter().getImplicit(); + case CAPTCHA -> properties.getGranter().getCaptcha(); + case SMS_CODE -> properties.getGranter().getSmsCode(); + case WECHAT_APPLET -> properties.getGranter().getWechatApplet(); + case SOCIAL -> properties.getGranter().getSocial(); + case REGISTER -> properties.getGranter().getRegister(); + default -> true; + }; + } +} diff --git a/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/handler/AbstractAuthorizationHandler.java b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/handler/AbstractAuthorizationHandler.java new file mode 100644 index 0000000..63c4517 --- /dev/null +++ b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/handler/AbstractAuthorizationHandler.java @@ -0,0 +1,91 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oauth2.handler; + +import org.springblade.core.oauth2.exception.ExceptionCode; +import org.springblade.core.oauth2.provider.OAuth2Request; +import org.springblade.core.oauth2.provider.OAuth2Validation; +import org.springblade.core.oauth2.service.OAuth2User; +import org.springblade.core.tool.utils.Func; + +/** + * AbstractAuthorizationHandler + * + * @author BladeX + */ +public abstract class AbstractAuthorizationHandler implements AuthorizationHandler { + + /** + * 认证校验 + * + * @param user 用户信息 + * @param request 请求信息 + * @return boolean + */ + @Override + public OAuth2Validation authValidation(OAuth2User user, OAuth2Request request) { + if (request.isClientCredentials() || request.isImplicit() || request.isSocial()) { + return new OAuth2Validation(); + } + if (Func.hasEmpty(user, user.getUserId())) { + return buildValidationFailure(ExceptionCode.USER_NOT_FOUND); + } + if (Func.isEmpty(user.getAuthorities())) { + return buildValidationFailure(ExceptionCode.UNAUTHORIZED_USER); + } + return new OAuth2Validation(); + } + + /** + * 认证成功回调 + * + * @param user 用户信息 + */ + @Override + public abstract void authSuccessful(OAuth2User user, OAuth2Request request); + + /** + * 认证失败回调 + * + * @param user 用户信息 + * @param validation 失败信息 + */ + @Override + public abstract void authFailure(OAuth2User user, OAuth2Request request, OAuth2Validation validation); + + /** + * 构建认证失败返回 + * + * @param errorCode 错误码 + * @return 认证结果 + */ + public OAuth2Validation buildValidationFailure(ExceptionCode errorCode) { + return new OAuth2Validation().setSuccess(false) + .setCode(errorCode.getCode()) + .setMessage(errorCode.getMessage()); + } + +} diff --git a/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/handler/AuthorizationHandler.java b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/handler/AuthorizationHandler.java new file mode 100644 index 0000000..03b683c --- /dev/null +++ b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/handler/AuthorizationHandler.java @@ -0,0 +1,62 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oauth2.handler; + +import org.springblade.core.oauth2.provider.OAuth2Request; +import org.springblade.core.oauth2.provider.OAuth2Validation; +import org.springblade.core.oauth2.service.OAuth2User; + +/** + * OAuth2AuthorizationHandler + * + * @author BladeX + */ +public interface AuthorizationHandler { + + /** + * 认证校验 + * + * @param user 用户信息 + * @param request 请求信息 + * @return boolean + */ + OAuth2Validation authValidation(OAuth2User user, OAuth2Request request); + + /** + * 认证成功回调 + * + * @param user 用户信息 + */ + void authSuccessful(OAuth2User user, OAuth2Request request); + + /** + * 认证失败回调 + * + * @param user 用户信息 + * @param validation 失败信息 + */ + void authFailure(OAuth2User user, OAuth2Request request, OAuth2Validation validation); +} diff --git a/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/handler/OAuth2AuthorizationHandler.java b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/handler/OAuth2AuthorizationHandler.java new file mode 100644 index 0000000..439aa3f --- /dev/null +++ b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/handler/OAuth2AuthorizationHandler.java @@ -0,0 +1,62 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oauth2.handler; + +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.oauth2.provider.OAuth2Request; +import org.springblade.core.oauth2.provider.OAuth2Validation; +import org.springblade.core.oauth2.service.OAuth2User; + +/** + * AbstractAuthorizationHandler + * + * @author BladeX + */ +@Slf4j +public class OAuth2AuthorizationHandler extends AbstractAuthorizationHandler { + + /** + * 认证成功回调 + * + * @param user 用户信息 + */ + @Override + public void authSuccessful(OAuth2User user, OAuth2Request request) { + + } + + /** + * 认证失败回调 + * + * @param user 用户信息 + * @param validation 失败信息 + */ + @Override + public void authFailure(OAuth2User user, OAuth2Request request, OAuth2Validation validation) { + log.error("用户:{},认证失败,失败原因:{}", user.getAccount(), validation.getMessage()); + } + +} diff --git a/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/handler/OAuth2PasswordHandler.java b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/handler/OAuth2PasswordHandler.java new file mode 100644 index 0000000..25eae93 --- /dev/null +++ b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/handler/OAuth2PasswordHandler.java @@ -0,0 +1,59 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oauth2.handler; + +import org.springblade.core.tool.utils.DigestUtil; + +/** + * BladePasswordHandler + * + * @author BladeX + */ +public class OAuth2PasswordHandler implements PasswordHandler { + + /** + * 判断密码是否匹配 + * + * @param rawPassword 请求时提交的原密码 + * @param encodedPassword 数据库加密后的密码 + * @return boolean + */ + @Override + public boolean matches(String rawPassword, String encodedPassword) { + return encodedPassword.equals(encode(rawPassword)); + } + + /** + * 加密密码规则 + * + * @param rawPassword 密码 + * @return 加密后的密码 + */ + @Override + public String encode(String rawPassword) { + return DigestUtil.hex(rawPassword); + } +} diff --git a/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/handler/OAuth2TokenHandler.java b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/handler/OAuth2TokenHandler.java new file mode 100644 index 0000000..feff9f1 --- /dev/null +++ b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/handler/OAuth2TokenHandler.java @@ -0,0 +1,80 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oauth2.handler; + +import lombok.RequiredArgsConstructor; +import org.springblade.core.jwt.JwtUtil; +import org.springblade.core.jwt.props.JwtProperties; +import org.springblade.core.oauth2.provider.OAuth2Request; +import org.springblade.core.oauth2.provider.OAuth2Token; +import org.springblade.core.oauth2.service.OAuth2User; + +/** + * BladeTokenHandler + * + * @author BladeX + */ +@RequiredArgsConstructor +public class OAuth2TokenHandler implements TokenHandler { + + private final JwtProperties properties; + + /** + * 令牌增强 + * + * @param user 用户信息 + * @param token 令牌信息 + * @param request 授权参数 + * @return OAuth2Token + */ + @Override + public OAuth2Token enhance(OAuth2User user, OAuth2Token token, OAuth2Request request) { + + //令牌状态配置, 仅在生成AccessToken时候执行 + if (properties.getState() && token.hasAccessToken()) { + JwtUtil.addAccessToken( + user.getTenantId(), + user.getClient().getClientId(), + user.getUserId(), + token.getAccessToken(), + token.getAccessTokenExpire() + ); + } + //令牌状态配置, 仅在生成RefreshToken时候执行 + if (properties.getState() && properties.getSingle() && token.hasRefreshToken()) { + JwtUtil.addRefreshToken( + user.getTenantId(), + user.getClient().getClientId(), + user.getUserId(), + token.getRefreshToken(), + token.getRefreshTokenExpire() + ); + } + + // 返回令牌 + return token; + } +} diff --git a/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/handler/PasswordHandler.java b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/handler/PasswordHandler.java new file mode 100644 index 0000000..3eddff3 --- /dev/null +++ b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/handler/PasswordHandler.java @@ -0,0 +1,51 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oauth2.handler; + +/** + * PasswordHandler + * + * @author BladeX + */ +public interface PasswordHandler { + + /** + * 判断密码是否匹配 + * + * @param rawPassword 请求时提交的原密码 + * @param encodedPassword 数据库加密后的密码 + * @return boolean + */ + boolean matches(String rawPassword, String encodedPassword); + + /** + * 加密密码规则 + * + * @param rawPassword 密码 + * @return 加密后的密码 + */ + String encode(String rawPassword); +} diff --git a/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/handler/TokenHandler.java b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/handler/TokenHandler.java new file mode 100644 index 0000000..0ba6436 --- /dev/null +++ b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/handler/TokenHandler.java @@ -0,0 +1,48 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oauth2.handler; + +import org.springblade.core.oauth2.provider.OAuth2Request; +import org.springblade.core.oauth2.provider.OAuth2Token; +import org.springblade.core.oauth2.service.OAuth2User; + +/** + * TokenHandler + * + * @author BladeX + */ +public interface TokenHandler { + + /** + * 令牌增强 + * + * @param user 用户信息 + * @param token 令牌信息 + * @param request 授权参数 + * @return OAuth2Token + */ + OAuth2Token enhance(OAuth2User user, OAuth2Token token, OAuth2Request request); +} diff --git a/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/props/OAuth2Properties.java b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/props/OAuth2Properties.java new file mode 100644 index 0000000..a17395c --- /dev/null +++ b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/props/OAuth2Properties.java @@ -0,0 +1,108 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oauth2.props; + +import lombok.Data; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * OAuth2Property + * + * @author BladeX + */ +@Getter +@Setter +@ConfigurationProperties(OAuth2Properties.PREFIX) +public class OAuth2Properties { + /** + * 配置前缀 + */ + public static final String PREFIX = "blade.oauth2"; + + /** + * 是否开启OAuth2 + */ + private Boolean enabled = true; + + /** + * code缓存时间 + */ + private long codeTimeout = 10 * 60L; + + /** + * 授权模式 + */ + private Granter granter = new Granter(); + + @Data + @NoArgsConstructor + public static class Granter { + /** + * 是否开启授权码模式 + */ + private Boolean authorizationCode = true; + /** + * 是否开启验证码模式 + */ + private Boolean captcha = true; + /** + * 是否开启密码模式 + */ + private Boolean password = true; + /** + * 是否开启刷新token模式 + */ + private Boolean refreshToken = true; + /** + * 是否开启客户端模式 + */ + private Boolean clientCredentials = true; + /** + * 是否开启简化模式 + */ + private Boolean implicit = true; + /** + * 是否手机验证码模式 + */ + private Boolean smsCode = true; + /** + * 是否开启微信小程序模式 + */ + private Boolean wechatApplet = true; + /** + * 是否开启开放平台模式 + */ + private Boolean social = true; + /** + * 是否开启注册模式 + */ + private Boolean register = true; + } + +} diff --git a/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/provider/OAuth2AuthorizationRequest.java b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/provider/OAuth2AuthorizationRequest.java new file mode 100644 index 0000000..c7b8215 --- /dev/null +++ b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/provider/OAuth2AuthorizationRequest.java @@ -0,0 +1,107 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oauth2.provider; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.Data; +import org.springblade.core.tool.utils.StringUtil; +import org.springblade.core.tool.utils.WebUtil; + +import java.io.Serial; +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import static org.springblade.core.oauth2.constant.OAuth2ParameterConstant.*; + +/** + * OAuth2AuthorizationRequest + * + * @author BladeX + */ +@Data +public class OAuth2AuthorizationRequest implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private String responseType; + private String tenantId; + private String clientId; + private String redirectUri; + private String scope; + private String state; + + /** + * 实例化 + */ + public static OAuth2AuthorizationRequest create() { + return new OAuth2AuthorizationRequest(); + } + + /** + * 构建参数 + * + * @return OAuth2AuthorizationRequest + */ + public OAuth2AuthorizationRequest buildParameters() { + HttpServletRequest request = Objects.requireNonNull(WebUtil.getRequest()); + this.responseType = request.getParameter(RESPONSE_TYPE); + this.tenantId = request.getParameter(TENANT_ID); + this.clientId = request.getParameter(CLIENT_ID); + this.redirectUri = request.getParameter(REDIRECT_URI); + this.scope = request.getParameter(SCOPE); + this.state = request.getParameter(STATE); + return this; + } + + /** + * 获取参数 + * + * @return Map + */ + public Map getParameters() { + Map parameters = new HashMap<>(); + parameters.put(RESPONSE_TYPE, this.responseType); + parameters.put(CLIENT_ID, this.clientId); + parameters.put(REDIRECT_URI, this.redirectUri); + if (scope != null) { + parameters.put(SCOPE, this.scope); + } + if (this.tenantId != null) { + parameters.put(STATE, this.tenantId); + } + if (state != null) { + parameters.put(STATE, this.state); + } + return parameters; + } + + public String getState() { + return StringUtil.isBlank(this.state) ? this.tenantId : this.state; + } +} diff --git a/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/provider/OAuth2Request.java b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/provider/OAuth2Request.java new file mode 100644 index 0000000..17ceda3 --- /dev/null +++ b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/provider/OAuth2Request.java @@ -0,0 +1,386 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oauth2.provider; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.Data; +import org.springblade.core.oauth2.utils.OAuth2Util; +import org.springblade.core.tool.support.Kv; +import org.springblade.core.tool.utils.StringUtil; +import org.springblade.core.tool.utils.WebUtil; + +import java.util.Arrays; +import java.util.Objects; +import java.util.Optional; + +import static org.springblade.core.oauth2.constant.OAuth2GranterConstant.PASSWORD; +import static org.springblade.core.oauth2.constant.OAuth2GranterConstant.REFRESH_TOKEN; +import static org.springblade.core.oauth2.constant.OAuth2GranterConstant.*; +import static org.springblade.core.oauth2.constant.OAuth2ParameterConstant.*; +import static org.springblade.core.oauth2.constant.OAuth2TokenConstant.*; + +/** + * OAuth2参数类 + * + * @author BladeX + */ +@Data +public class OAuth2Request { + + /** + * 实例化 + */ + public static OAuth2Request create() { + return new OAuth2Request(); + } + + /** + * 请求参数 + */ + private Kv parameterArgs = Kv.create(); + + /** + * 头部参数 + */ + private Kv headerArgs = Kv.create(); + + /** + * 自动构建参数 + * + * @return OAuth2Request + */ + public OAuth2Request buildArgs() { + return this.buildParameterArgs().buildHeaderArgs(); + } + + /** + * 自动构建请求头参数 + * + * @return OAuth2Request + */ + public OAuth2Request buildParameterArgs() { + HttpServletRequest request = Objects.requireNonNull(WebUtil.getRequest()); + Arrays.stream(new String[]{ + CLIENT_ID, CLIENT_SECRET, ACCESS_TOKEN, REFRESH_TOKEN, TENANT_ID, USERNAME, PASSWORD, NAME, PHONE, EMAIL, GRANT_TYPE, SCOPE, REDIRECT_URI, RESPONSE_TYPE, CODE, STATE, SOURCE + }).forEach(param -> Optional.ofNullable(request.getParameter(param)).ifPresent(value -> parameterArgs.set(param, value))); + return this; + } + + /** + * 自动构建头部参数 + * + * @return OAuth2Request + */ + public OAuth2Request buildHeaderArgs() { + HttpServletRequest request = Objects.requireNonNull(WebUtil.getRequest()); + Arrays.stream(new String[]{ + HEADER_AUTHORIZATION, TENANT_HEADER, USER_HEADER, ROLE_HEADER, DEPT_HEADER, USER_TYPE_HEADER, CAPTCHA_HEADER_KEY, CAPTCHA_HEADER_CODE + }).forEach(param -> Optional.ofNullable(request.getHeader(param)).ifPresent(value -> headerArgs.set(param, value))); + return this; + } + + /** + * 获取客户端ID + * + * @return String + */ + public String getClientIdFromParameter() { + return parameterArgs.getStr(CLIENT_ID); + } + + /** + * 获取客户端密钥 + * + * @return String + */ + public String getClientSecretFromParameter() { + return parameterArgs.getStr(CLIENT_SECRET); + } + + /** + * 获取客户端ID和密钥 + * + * @return String[] + */ + public String[] getClientFromAuthorization() { + return OAuth2Util.extractAndDecodeAuthorization(); + } + + /** + * 获取令牌 + * + * @return String + */ + public String getToken() { + return headerArgs.getStr(TOKEN_HEADER); + } + + /** + * 获取租户编号 + * + * @return String + */ + public String getTenantId() { + if (StringUtil.isBlank(headerArgs.getStr(TENANT_HEADER))) { + return parameterArgs.getStr(TENANT_ID); + } + return headerArgs.getStr(TENANT_HEADER); + } + + /** + * 获取用户ID + * + * @return String + */ + public String getUserId() { + return headerArgs.getStr(USER_HEADER); + } + + /** + * 获取用户名 + * + * @return String + */ + public String getUsername() { + return parameterArgs.getStr(USERNAME); + } + + /** + * 获取密码 + * + * @return String + */ + public String getPassword() { + return parameterArgs.getStr(PASSWORD); + } + + /** + * 获取用户名字 + * + * @return String + */ + public String getName() { + return parameterArgs.getStr(NAME); + } + + /** + * 获取手机号 + * + * @return String + */ + public String getPhone() { + return parameterArgs.getStr(PHONE); + } + + /** + * 获取电子游戏 + * + * @return String + */ + public String getEmail() { + return parameterArgs.getStr(EMAIL); + } + + /** + * 获取用户名 + * + * @return String + */ + public String getUserType() { + return headerArgs.getStr(USER_TYPE_HEADER); + } + + /** + * 获取用户部门 + * + * @return String + */ + public String getUserDept() { + return headerArgs.getStr(DEPT_HEADER); + } + + + /** + * 获取用户角色 + * + * @return String + */ + public String getUserRole() { + return headerArgs.getStr(ROLE_HEADER); + } + + + /** + * 获取验证码key + */ + public String getCaptchaKey() { + return headerArgs.getStr(CAPTCHA_HEADER_KEY); + } + + /** + * 获取验证码code + */ + public String getCaptchaCode() { + return headerArgs.getStr(CAPTCHA_HEADER_CODE); + } + + /** + * 获取授权类型 + * + * @return String + */ + public String getGrantType() { + return parameterArgs.getStr(GRANT_TYPE); + } + + /** + * 获取刷新令牌 + * + * @return String + */ + public String getRefreshToken() { + return parameterArgs.getStr(REFRESH_TOKEN); + } + + /** + * 获取验证code + * + * @return String + */ + public String getCode() { + return parameterArgs.getStr(CODE); + } + + /** + * 获取状态 + * + * @return String + */ + public String getState() { + return parameterArgs.getStr(STATE); + } + + /** + * 获取来源 + * + * @return String + */ + public String getSource() { + return parameterArgs.getStr(SOURCE); + } + + /** + * 获取回调地址 + * + * @return String + */ + public String getRedirectUri() { + return parameterArgs.getStr(REDIRECT_URI); + } + + /** + * 是否密码模式 + * + * @return Boolean + */ + public Boolean isPassword() { + return PASSWORD.equals(getGrantType()); + } + + /** + * 是否刷新模式 + * + * @return Boolean + */ + public Boolean isRefreshToken() { + return REFRESH_TOKEN.equals(getGrantType()); + } + + /** + * 是否验证码模式 + * + * @return Boolean + */ + public Boolean isCaptchaCode() { + return CAPTCHA.equals(getGrantType()); + } + + /** + * 是否密码模式 + * + * @return Boolean + */ + public Boolean isClientCredentials() { + return CLIENT_CREDENTIALS.equals(getGrantType()); + } + + /** + * 是否简化模式 + * + * @return Boolean + */ + public Boolean isImplicit() { + return IMPLICIT.equals(getGrantType()); + } + + + /** + * 是否开放平台模式 + * + * @return Boolean + */ + public Boolean isSocial() { + return SOCIAL.equals(getGrantType()); + } + + /** + * 设置租户ID + * + * @param tenantId 户ID + */ + public void setTenantId(String tenantId) { + this.headerArgs.set(TENANT_HEADER, tenantId); + } + + /** + * 设置用户ID + * + * @param userId 用户ID + */ + public void setUserId(String userId) { + this.headerArgs.set(USER_HEADER, userId); + } + + /** + * 设置用户名 + * + * @param username 用户名 + */ + public void setUsername(String username) { + this.parameterArgs.set(USERNAME, username); + } + +} diff --git a/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/provider/OAuth2Response.java b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/provider/OAuth2Response.java new file mode 100644 index 0000000..b381f9f --- /dev/null +++ b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/provider/OAuth2Response.java @@ -0,0 +1,73 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oauth2.provider; + +import lombok.Data; +import org.springblade.core.tool.support.Kv; + +import java.io.Serial; +import java.io.Serializable; + +import static org.springblade.core.oauth2.constant.OAuth2ResponseConstant.*; + +/** + * OAuth2Response + * + * @author BladeX + */ +@Data +public class OAuth2Response implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 实例化 + */ + public static OAuth2Response create() { + return new OAuth2Response(); + } + + /** + * 响应参数 + */ + private Kv args = Kv.create(); + + public Kv of(boolean success, int errorCode, String errorDescription) { + args.set(SUCCESS, success); + args.set(ERROR_CODE, errorCode); + args.set(ERROR_DESCRIPTION, errorDescription); + return args; + + } + + public Kv ofValidation(OAuth2Validation validation) { + args.set(SUCCESS, validation.isSuccess()); + args.set(ERROR_CODE, validation.getCode()); + args.set(ERROR_DESCRIPTION, validation.getMessage()); + return args; + } +} diff --git a/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/provider/OAuth2Token.java b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/provider/OAuth2Token.java new file mode 100644 index 0000000..78fd4fb --- /dev/null +++ b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/provider/OAuth2Token.java @@ -0,0 +1,99 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oauth2.provider; + +import lombok.Data; +import lombok.experimental.Accessors; +import org.springblade.core.tool.support.Kv; +import org.springblade.core.tool.utils.StringUtil; + +import java.io.Serial; +import java.io.Serializable; + +/** + * OAuth2Token + * + * @author BladeX + */ +@Data +@Accessors(chain = true) +public class OAuth2Token implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 实例化 + */ + public static OAuth2Token create() { + return new OAuth2Token(); + } + + /** + * 令牌值 + */ + private String accessToken; + + /** + * 刷新令牌值 + */ + private String refreshToken; + + /** + * 令牌过期秒数 + */ + private int accessTokenExpire; + + /** + * 刷新令牌过期秒数 + */ + private int refreshTokenExpire; + + /** + * 令牌参数 + */ + private Kv args = Kv.create(); + + + /** + * 是否包含令牌 + * + * @return Boolean + */ + public Boolean hasAccessToken() { + return StringUtil.isNotBlank(accessToken); + } + + /** + * 是否包含刷新令牌 + * + * @return Boolean + */ + public Boolean hasRefreshToken() { + return StringUtil.isNotBlank(refreshToken); + } + +} diff --git a/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/provider/OAuth2Validation.java b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/provider/OAuth2Validation.java new file mode 100644 index 0000000..639cae4 --- /dev/null +++ b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/provider/OAuth2Validation.java @@ -0,0 +1,55 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oauth2.provider; + +import lombok.Data; +import lombok.experimental.Accessors; + +/** + * OAuth2Validation + * + * @author BladeX + */ +@Data +@Accessors(chain = true) +public class OAuth2Validation { + + /** + * 是否成功 + */ + boolean success = true; + + /** + * 状态码 + */ + int code = 1000; + + /** + * 验证信息 + */ + String message = "认证通过"; + +} diff --git a/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/service/OAuth2Client.java b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/service/OAuth2Client.java new file mode 100644 index 0000000..bba3794 --- /dev/null +++ b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/service/OAuth2Client.java @@ -0,0 +1,113 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oauth2.service; + +import java.io.Serializable; + +/** + * 多终端详情接口 + * + * @author BladeX + */ +public interface OAuth2Client extends Serializable { + + /** + * 获取客户端ID. + * + * @return String + */ + String getClientId(); + + /** + * 获取客户端密钥. + * + * @return String + */ + String getClientSecret(); + + /** + * 获取资源集合. + * + * @return String + */ + String getResourceIds(); + + /** + * 获取授权范围. + * + * @return String + */ + String getScope(); + + /** + * 获取授权类型. + * + * @return String + */ + String getAuthorizedGrantTypes(); + + /** + * 获取回调地址. + * + * @return String + */ + String getWebServerRedirectUri(); + + /** + * 获取权限. + * + * @return String + */ + String getAuthorities(); + + /** + * 获取访问令牌有效期. + * + * @return Integer + */ + Integer getAccessTokenValidity(); + + /** + * 获取刷新令牌有效期. + * + * @return Integer + */ + Integer getRefreshTokenValidity(); + + /** + * 获取附加信息. + * + * @return String + */ + String getAdditionalInformation(); + + /** + * 获取自动授权. + * + * @return String + */ + String getAutoapprove(); +} diff --git a/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/service/OAuth2ClientService.java b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/service/OAuth2ClientService.java new file mode 100644 index 0000000..ecf2776 --- /dev/null +++ b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/service/OAuth2ClientService.java @@ -0,0 +1,82 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oauth2.service; + +import org.springblade.core.oauth2.provider.OAuth2Request; + +/** + * 多终端注册接口 + * + * @author BladeX + */ +public interface OAuth2ClientService { + + /** + * 根据clientId获取Client详情 + * + * @param clientId 客户端id + * @return 客户端信息 + */ + OAuth2Client loadByClientId(String clientId); + + /** + * 根据clientId获取Client详情 + * + * @param clientId 客户端id + * @param request 授权参数 + * @return 客户端信息 + */ + OAuth2Client loadByClientId(String clientId, OAuth2Request request); + + /** + * 验证Client信息 + * + * @param client client信息 + * @param clientId 客户端id + * @param clientSecret 客户端密钥 + * @return boolean + */ + boolean validateClient(OAuth2Client client, String clientId, String clientSecret); + + /** + * 验证Client信息 + * + * @param client client信息 + * @param redirectUri 回调地址 + * @return boolean + */ + boolean validateRedirectUri(OAuth2Client client, String redirectUri); + + /** + * 验证授权类型 + * + * @param client client信息 + * @param grantType 授权类型 + * @return boolean + */ + boolean validateGranter(OAuth2Client client, String grantType); + +} diff --git a/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/service/OAuth2User.java b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/service/OAuth2User.java new file mode 100644 index 0000000..0bf7527 --- /dev/null +++ b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/service/OAuth2User.java @@ -0,0 +1,170 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oauth2.service; + +import org.springblade.core.tool.support.Kv; + +import java.io.Serializable; +import java.util.List; + +/** + * 用户基础信息 + * + * @author BladeX + */ +public interface OAuth2User extends Serializable { + + /** + * 获取用户ID. + * + * @return Long + */ + String getUserId(); + + /** + * 获取租户ID. + * + * @return String + */ + String getTenantId(); + + /** + * 获取第三方认证ID. + * + * @return String + */ + String getOauthId(); + + /** + * 获取昵称. + * + * @return String + */ + String getName(); + + /** + * 获取真名. + * + * @return String + */ + String getRealName(); + + /** + * 获取账号. + * + * @return String + */ + String getAccount(); + + /** + * 获取密码. + * + * @return String + */ + String getPassword(); + + /** + * 获取手机. + * + * @return String + */ + String getPhone(); + + /** + * 获取邮箱. + * + * @return String + */ + String getEmail(); + + /** + * 获取部门ID. + * + * @return String + */ + String getDeptId(); + + /** + * 获取岗位ID. + * + * @return String + */ + String getPostId(); + + /** + * 获取角色ID. + * + * @return String + */ + String getRoleId(); + + /** + * 获取角色名. + * + * @return String + */ + String getRoleName(); + + /** + * 获取头像. + * + * @return String + */ + String getAvatar(); + + /** + * 获取权限集合. + * + * @return List + */ + List getPermissions(); + + /** + * 获取角色集合. + * + * @return List + */ + List getAuthorities(); + + /** + * 获取客户端信息. + * + * @return OAuth2Client + */ + OAuth2Client getClient(); + + /** + * 设置客户端信息. + */ + void setClient(OAuth2Client client); + + /** + * 获取用户详情. + * + * @return Kv + */ + Kv getDetail(); +} diff --git a/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/service/OAuth2UserService.java b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/service/OAuth2UserService.java new file mode 100644 index 0000000..711db74 --- /dev/null +++ b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/service/OAuth2UserService.java @@ -0,0 +1,63 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oauth2.service; + +import org.springblade.core.oauth2.provider.OAuth2Request; + +/** + * OAuth2UserService + * + * @author BladeX + */ +public interface OAuth2UserService { + + /** + * 根据用户名获取用户信息 + * + * @param userId 用户ID + * @param request 授权参数 + * @return 用户信息 + */ + OAuth2User loadByUserId(String userId, OAuth2Request request); + + /** + * 根据用户名获取用户信息 + * + * @param username 用户名 + * @param request 授权参数 + * @return 用户信息 + */ + OAuth2User loadByUsername(String username, OAuth2Request request); + + /** + * 校验用户信息 + * + * @param user 用户信息 + * @return 是否通过 + */ + boolean validateUser(OAuth2User user); + +} diff --git a/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/service/impl/OAuth2ClientDetail.java b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/service/impl/OAuth2ClientDetail.java new file mode 100644 index 0000000..5bd221d --- /dev/null +++ b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/service/impl/OAuth2ClientDetail.java @@ -0,0 +1,102 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oauth2.service.impl; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springblade.core.oauth2.service.OAuth2Client; + +import java.io.Serial; + +/** + * 客户端详情 + * + * @author BladeX + */ +@Data +@Schema(description = "oauth2客户端实体类") +public class OAuth2ClientDetail implements OAuth2Client { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 客户端id + */ + @Schema(description = "客户端id") + private String clientId; + /** + * 客户端密钥 + */ + @Schema(description = "客户端密钥") + private String clientSecret; + /** + * 资源集合 + */ + @Schema(description = "资源集合") + private String resourceIds; + /** + * 授权范围 + */ + @Schema(description = "授权范围") + private String scope; + /** + * 授权类型 + */ + @Schema(description = "授权类型") + private String authorizedGrantTypes; + /** + * 回调地址 + */ + @Schema(description = "回调地址") + private String webServerRedirectUri; + /** + * 权限 + */ + @Schema(description = "权限") + private String authorities; + /** + * 令牌过期秒数 + */ + @Schema(description = "令牌过期秒数") + private Integer accessTokenValidity; + /** + * 刷新令牌过期秒数 + */ + @Schema(description = "刷新令牌过期秒数") + private Integer refreshTokenValidity; + /** + * 附加说明 + */ + @Schema(description = "附加说明") + private String additionalInformation; + /** + * 自动授权 + */ + @Schema(description = "自动授权") + private String autoapprove; + +} diff --git a/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/service/impl/OAuth2ClientDetailService.java b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/service/impl/OAuth2ClientDetailService.java new file mode 100644 index 0000000..86eb406 --- /dev/null +++ b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/service/impl/OAuth2ClientDetailService.java @@ -0,0 +1,88 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oauth2.service.impl; + +import lombok.AllArgsConstructor; +import org.springblade.core.oauth2.constant.OAuth2ClientConstant; +import org.springblade.core.oauth2.provider.OAuth2Request; +import org.springblade.core.oauth2.service.OAuth2Client; +import org.springblade.core.oauth2.service.OAuth2ClientService; +import org.springblade.core.tool.utils.Func; +import org.springblade.core.tool.utils.StringPool; +import org.springblade.core.tool.utils.StringUtil; +import org.springframework.jdbc.core.BeanPropertyRowMapper; +import org.springframework.jdbc.core.JdbcTemplate; + +import java.util.Arrays; +import java.util.Optional; + +/** + * 获取客户端详情 + * + * @author BladeX + */ +@AllArgsConstructor +public class OAuth2ClientDetailService implements OAuth2ClientService { + + private final JdbcTemplate jdbcTemplate; + + @Override + public OAuth2Client loadByClientId(String clientId) { + return loadByClientId(clientId, null); + } + + @Override + public OAuth2Client loadByClientId(String clientId, OAuth2Request request) { + try { + return jdbcTemplate.queryForObject(OAuth2ClientConstant.DEFAULT_SELECT_STATEMENT, new BeanPropertyRowMapper<>(OAuth2ClientDetail.class), clientId); + } catch (Exception ex) { + return null; + } + } + + @Override + public boolean validateClient(OAuth2Client client, String clientId, String clientSecret) { + return Optional.ofNullable(client) + .map(c -> StringUtil.equals(clientId, c.getClientId()) && StringUtil.equals(clientSecret, c.getClientSecret())) + .orElse(false); + } + + @Override + public boolean validateRedirectUri(OAuth2Client client, String redirectUri) { + return Optional.ofNullable(client) + .map(c -> StringUtil.equals(redirectUri, c.getWebServerRedirectUri())) + .orElse(false); + } + + @Override + public boolean validateGranter(OAuth2Client client, String grantType) { + return Optional.ofNullable(client) + .map(c -> Arrays.stream(Func.split(c.getAuthorizedGrantTypes(), StringPool.COMMA)) + .anyMatch(s -> s.trim().equals(grantType))) + .orElse(false); + } + +} diff --git a/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/service/impl/OAuth2UserDetail.java b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/service/impl/OAuth2UserDetail.java new file mode 100644 index 0000000..95fd123 --- /dev/null +++ b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/service/impl/OAuth2UserDetail.java @@ -0,0 +1,145 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oauth2.service.impl; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springblade.core.oauth2.service.OAuth2Client; +import org.springblade.core.oauth2.service.OAuth2User; +import org.springblade.core.tool.support.Kv; + +import java.io.Serial; +import java.util.List; + +/** + * 用户详情 + * + * @author BladeX + */ +@Data +@Schema(description = "oauth2用户实体类") +public class OAuth2UserDetail implements OAuth2User { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 用户id + */ + @Schema(description = "用户id") + private String userId; + /** + * 租户ID + */ + @Schema(description = "租户ID") + private String tenantId; + /** + * 第三方认证ID + */ + @Schema(description = "第三方认证ID") + private String oauthId; + /** + * 昵称 + */ + @Schema(description = "昵称") + private String name; + /** + * 真名 + */ + @Schema(description = "真名") + private String realName; + /** + * 账号 + */ + @Schema(description = "账号") + private String account; + /** + * 密码 + */ + @JsonIgnore + @Schema(description = "密码") + private String password; + /** + * 手机 + */ + @Schema(description = "手机") + private String phone; + /** + * 邮箱 + */ + @Schema(description = "邮箱") + private String email; + /** + * 部门id + */ + @Schema(description = "部门id") + private String deptId; + /** + * 岗位id + */ + @Schema(description = "岗位id") + private String postId; + /** + * 角色id + */ + @Schema(description = "角色id") + private String roleId; + /** + * 角色名 + */ + @Schema(description = "角色名") + private String roleName; + /** + * 头像 + */ + @Schema(description = "头像") + private String avatar; + + /** + * 权限标识集合 + */ + @Schema(description = "权限集合") + private List permissions; + + /** + * 角色集合 + */ + @Schema(description = "角色集合") + private List authorities; + + /** + * 客户端 + */ + @Schema(description = "客户端") + private OAuth2Client client; + + /** + * 用户详情 + */ + @Schema(description = "用户详情") + private Kv detail; +} diff --git a/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/service/impl/OAuth2UserDetailService.java b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/service/impl/OAuth2UserDetailService.java new file mode 100644 index 0000000..c466ff0 --- /dev/null +++ b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/service/impl/OAuth2UserDetailService.java @@ -0,0 +1,73 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oauth2.service.impl; + +import lombok.AllArgsConstructor; +import org.springblade.core.oauth2.constant.OAuth2UserConstant; +import org.springblade.core.oauth2.provider.OAuth2Request; +import org.springblade.core.oauth2.service.OAuth2User; +import org.springblade.core.oauth2.service.OAuth2UserService; +import org.springframework.jdbc.core.BeanPropertyRowMapper; +import org.springframework.jdbc.core.JdbcTemplate; + +import java.util.Optional; + +/** + * 获取用户详情 + * + * @author BladeX + */ +@AllArgsConstructor +public class OAuth2UserDetailService implements OAuth2UserService { + + private final JdbcTemplate jdbcTemplate; + + @Override + public OAuth2User loadByUserId(String userId, OAuth2Request request) { + try { + return jdbcTemplate.queryForObject(OAuth2UserConstant.DEFAULT_USERID_SELECT_STATEMENT, new BeanPropertyRowMapper<>(OAuth2UserDetail.class), userId); + } catch (Exception ex) { + return null; + } + } + + @Override + public OAuth2User loadByUsername(String username, OAuth2Request request) { + try { + return jdbcTemplate.queryForObject(OAuth2UserConstant.DEFAULT_USERNAME_SELECT_STATEMENT, new BeanPropertyRowMapper<>(OAuth2UserDetail.class), username); + } catch (Exception ex) { + return null; + } + } + + @Override + public boolean validateUser(OAuth2User user) { + return Optional.ofNullable(user) + .filter(u -> u.getUserId() != null && !u.getUserId().isEmpty()) // 检查userId不为空 + .filter(u -> u.getAuthorities() != null && !u.getAuthorities().isEmpty()) // 检查authorities不为空 + .isPresent(); // 如果上述条件都满足,则返回true,否则返回false + } +} diff --git a/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/utils/OAuth2CodeUtil.java b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/utils/OAuth2CodeUtil.java new file mode 100644 index 0000000..5842142 --- /dev/null +++ b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/utils/OAuth2CodeUtil.java @@ -0,0 +1,49 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oauth2.utils; + +/** + * OAuth2CodeUtil + * + * @author Chill + */ +public class OAuth2CodeUtil { + + /** + * 授权码缓存key + */ + public static final String AUTHORIZATION_CODE_KEY = "blade:auth::code:"; + + /** + * code key格式 + * + * @param code code + * @return key + */ + public static String codeKey(String code) { + return AUTHORIZATION_CODE_KEY + code; + } +} diff --git a/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/utils/OAuth2ExceptionUtil.java b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/utils/OAuth2ExceptionUtil.java new file mode 100644 index 0000000..2987aae --- /dev/null +++ b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/utils/OAuth2ExceptionUtil.java @@ -0,0 +1,116 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oauth2.utils; + +import org.springblade.core.oauth2.exception.*; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Supplier; + +import static org.springblade.core.oauth2.exception.OAuth2ErrorMessage.INVALID_ERROR_CODE; + +/** + * OAuth2ExceptionUtil + * + * @author BladeX + */ +public class OAuth2ExceptionUtil { + private static final Map> OAUTH2_EXCEPTION = new ConcurrentHashMap<>(16); + + static { + // 初始化异常映射 + OAUTH2_EXCEPTION.put( + ExceptionCode.INVALID_REQUEST, () -> new OAuth2Exception(ExceptionCode.INVALID_REQUEST, ExceptionCode.INVALID_REQUEST.getMessage()) + ); + OAUTH2_EXCEPTION.put( + ExceptionCode.USER_NOT_FOUND, () -> new UsernameNotFoundException(ExceptionCode.USER_NOT_FOUND.getMessage()) + ); + OAUTH2_EXCEPTION.put( + ExceptionCode.USER_TENANT_NOT_FOUND, () -> new UserInvalidException(ExceptionCode.USER_TENANT_NOT_FOUND.getMessage()) + ); + OAUTH2_EXCEPTION.put( + ExceptionCode.USER_TOO_MANY_FAILS, () -> new UserInvalidException(ExceptionCode.USER_TOO_MANY_FAILS.getMessage()) + ); + OAUTH2_EXCEPTION.put( + ExceptionCode.INVALID_USER, () -> new UserInvalidException(ExceptionCode.INVALID_USER.getMessage()) + ); + OAUTH2_EXCEPTION.put( + ExceptionCode.UNAUTHORIZED_USER, () -> new UserUnauthorizedException(ExceptionCode.UNAUTHORIZED_USER.getMessage()) + ); + OAUTH2_EXCEPTION.put( + ExceptionCode.UNAUTHORIZED_USER_TENANT, () -> new UserUnauthorizedException(ExceptionCode.UNAUTHORIZED_USER_TENANT.getMessage()) + ); + OAUTH2_EXCEPTION.put( + ExceptionCode.INVALID_REFRESH_TOKEN, () -> new GranterInvalidException(ExceptionCode.INVALID_REFRESH_TOKEN.getMessage()) + ); + OAUTH2_EXCEPTION.put( + ExceptionCode.CLIENT_NOT_FOUND, () -> new ClientNotFoundException(ExceptionCode.CLIENT_NOT_FOUND.getMessage()) + ); + OAUTH2_EXCEPTION.put( + ExceptionCode.INVALID_CLIENT, () -> new ClientInvalidException(ExceptionCode.INVALID_CLIENT.getMessage()) + ); + OAUTH2_EXCEPTION.put( + ExceptionCode.INVALID_CLIENT_REDIRECT_URI, () -> new ClientInvalidException(ExceptionCode.INVALID_CLIENT_REDIRECT_URI.getMessage()) + ); + OAUTH2_EXCEPTION.put( + ExceptionCode.UNAUTHORIZED_CLIENT, () -> new ClientUnauthorizedException(ExceptionCode.UNAUTHORIZED_CLIENT.getMessage()) + ); + OAUTH2_EXCEPTION.put( + ExceptionCode.UNSUPPORTED_GRANT_TYPE, () -> new OAuth2Exception(ExceptionCode.UNSUPPORTED_GRANT_TYPE, ExceptionCode.UNSUPPORTED_GRANT_TYPE.getMessage()) + ); + OAUTH2_EXCEPTION.put( + ExceptionCode.INVALID_GRANTER, () -> new GranterInvalidException(ExceptionCode.INVALID_GRANTER.getMessage()) + ); + OAUTH2_EXCEPTION.put( + ExceptionCode.INVALID_SCOPE, () -> new OAuth2Exception(ExceptionCode.INVALID_SCOPE, ExceptionCode.INVALID_SCOPE.getMessage()) + ); + OAUTH2_EXCEPTION.put( + ExceptionCode.SERVER_ERROR, () -> new OAuth2Exception(ExceptionCode.SERVER_ERROR, ExceptionCode.SERVER_ERROR.getMessage()) + ); + OAUTH2_EXCEPTION.put( + ExceptionCode.ACCESS_DENIED, () -> new OAuth2Exception(ExceptionCode.ACCESS_DENIED, ExceptionCode.ACCESS_DENIED.getMessage()) + ); + OAUTH2_EXCEPTION.put( + ExceptionCode.TEMPORARILY_UNAVAILABLE, () -> new OAuth2Exception(ExceptionCode.TEMPORARILY_UNAVAILABLE, ExceptionCode.TEMPORARILY_UNAVAILABLE.getMessage()) + ); + } + + /** + * 根据错误代码抛出异常 + * + * @param code 错误代码 + */ + public static void throwFromCode(int code) { + Supplier exceptionSupplier = OAUTH2_EXCEPTION.get(ExceptionCode.of(code)); + if (exceptionSupplier != null) { + throw exceptionSupplier.get(); + } else { + throw new IllegalArgumentException(String.format(INVALID_ERROR_CODE, code)); + } + } + +} diff --git a/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/utils/OAuth2Util.java b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/utils/OAuth2Util.java new file mode 100644 index 0000000..9ea2e83 --- /dev/null +++ b/blade-core-oauth2/src/main/java/org/springblade/core/oauth2/utils/OAuth2Util.java @@ -0,0 +1,107 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oauth2.utils; + +import org.springblade.core.oauth2.service.OAuth2Client; +import org.springblade.core.oauth2.service.OAuth2User; +import org.springblade.core.secure.TokenInfo; +import org.springblade.core.secure.utils.SecureUtil; +import org.springblade.core.tool.support.Kv; + +import static org.springblade.core.launch.constant.TokenConstant.*; + +/** + * OAuth2Util + * + * @author BladeX + */ +public class OAuth2Util extends SecureUtil { + + /** + * 创建accessToken + * + * @param user 用户信息 + * @return accessToken + */ + public static TokenInfo createAccessToken(OAuth2User user) { + Kv kv = Kv.create().set(TOKEN_TYPE, ACCESS_TOKEN) + .set(CLIENT_ID, user.getClient().getClientId()) + .set(TENANT_ID, user.getTenantId()) + .set(USER_ID, user.getUserId()) + .set(DEPT_ID, user.getDeptId()) + .set(POST_ID, user.getPostId()) + .set(ROLE_ID, user.getRoleId()) + .set(OAUTH_ID, user.getOauthId()) + .set(ACCOUNT, user.getAccount()) + .set(USER_NAME, user.getAccount()) + .set(NICK_NAME, user.getName()) + .set(REAL_NAME, user.getRealName()) + .set(ROLE_NAME, user.getRoleName()) + .set(DETAIL, user.getDetail()); + return createToken(kv, user.getClient().getAccessTokenValidity()); + } + + /** + * 创建refreshToken + * + * @param user 用户信息 + * @return refreshToken + */ + public static TokenInfo createRefreshToken(OAuth2User user) { + Kv kv = Kv.create().set(TOKEN_TYPE, REFRESH_TOKEN) + .set(USER_ID, user.getUserId()) + .set(DEPT_ID, user.getDeptId()) + .set(ROLE_ID, user.getRoleId()); + return createToken(kv, user.getClient().getRefreshTokenValidity()); + } + + + /** + * 创建clientAccessToken + * + * @param client 客户端信息 + * @return clientToken + */ + public static TokenInfo createClientAccessToken(OAuth2Client client) { + Kv kv = Kv.create().set(TOKEN_TYPE, CLIENT_ACCESS_TOKEN) + .set(CLIENT_ID, client.getClientId()); + return createToken(kv, client.getAccessTokenValidity()); + } + + /** + * 创建implicitAccessToken + * + * @param user 用户信息 + * @return implicitAccessToken + */ + public static TokenInfo createImplicitAccessToken(OAuth2User user) { + Kv kv = Kv.create().set(TOKEN_TYPE, IMPLICIT_ACCESS_TOKEN) + .set(ACCOUNT, user.getAccount()); + return createToken(kv); + } + + +} diff --git a/blade-core-oauth2/src/main/resources/static/css/bootstrap.min.css b/blade-core-oauth2/src/main/resources/static/css/bootstrap.min.css new file mode 100644 index 0000000..493c086 --- /dev/null +++ b/blade-core-oauth2/src/main/resources/static/css/bootstrap.min.css @@ -0,0 +1,6 @@ +/*! + * Bootstrap v4.6.1 (https://getbootstrap.com/) + * Copyright 2011-2021 The Bootstrap Authors + * Copyright 2011-2021 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */:root{--blue:#007bff;--indigo:#6610f2;--purple:#6f42c1;--pink:#e83e8c;--red:#dc3545;--orange:#fd7e14;--yellow:#ffc107;--green:#28a745;--teal:#20c997;--cyan:#17a2b8;--white:#fff;--gray:#6c757d;--gray-dark:#343a40;--primary:#007bff;--secondary:#6c757d;--success:#28a745;--info:#17a2b8;--warning:#ffc107;--danger:#dc3545;--light:#f8f9fa;--dark:#343a40;--breakpoint-xs:0;--breakpoint-sm:576px;--breakpoint-md:768px;--breakpoint-lg:992px;--breakpoint-xl:1200px;--font-family-sans-serif:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-family-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus:not(:focus-visible){outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([class]){color:inherit;text-decoration:none}a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit;text-align:-webkit-match-parent}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:2.5rem}.h2,h2{font-size:2rem}.h3,h3{font-size:1.75rem}.h4,h4{font-size:1.5rem}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:6rem;font-weight:300;line-height:1.2}.display-2{font-size:5.5rem;font-weight:300;line-height:1.2}.display-3{font-size:4.5rem;font-weight:300;line-height:1.2}.display-4{font-size:3.5rem;font-weight:300;line-height:1.2}hr{margin-top:1rem;margin-bottom:1rem;border:0;border-top:1px solid rgba(0,0,0,.1)}.small,small{font-size:80%;font-weight:400}.mark,mark{padding:.2em;background-color:#fcf8e3}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:90%;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote-footer{display:block;font-size:80%;color:#6c757d}.blockquote-footer::before{content:"\2014\00A0"}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:90%;color:#6c757d}code{font-size:87.5%;color:#e83e8c;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:87.5%;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:100%;font-weight:700}pre{display:block;font-size:87.5%;color:#212529}pre code{font-size:inherit;color:inherit;word-break:normal}.pre-scrollable{max-height:340px;overflow-y:scroll}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}.row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*=col-]{padding-right:0;padding-left:0}.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-auto,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-auto,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-auto,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-auto,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-auto{position:relative;width:100%;padding-right:15px;padding-left:15px}.col{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-first{-ms-flex-order:-1;order:-1}.order-last{-ms-flex-order:13;order:13}.order-0{-ms-flex-order:0;order:0}.order-1{-ms-flex-order:1;order:1}.order-2{-ms-flex-order:2;order:2}.order-3{-ms-flex-order:3;order:3}.order-4{-ms-flex-order:4;order:4}.order-5{-ms-flex-order:5;order:5}.order-6{-ms-flex-order:6;order:6}.order-7{-ms-flex-order:7;order:7}.order-8{-ms-flex-order:8;order:8}.order-9{-ms-flex-order:9;order:9}.order-10{-ms-flex-order:10;order:10}.order-11{-ms-flex-order:11;order:11}.order-12{-ms-flex-order:12;order:12}.offset-1{margin-left:8.333333%}.offset-2{margin-left:16.666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.333333%}.offset-5{margin-left:41.666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.333333%}.offset-8{margin-left:66.666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.333333%}.offset-11{margin-left:91.666667%}@media (min-width:576px){.col-sm{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-sm-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-sm-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-sm-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-sm-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-sm-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-sm-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-sm-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-sm-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-sm-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-sm-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-sm-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-sm-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-sm-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-sm-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-sm-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-sm-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-sm-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-sm-first{-ms-flex-order:-1;order:-1}.order-sm-last{-ms-flex-order:13;order:13}.order-sm-0{-ms-flex-order:0;order:0}.order-sm-1{-ms-flex-order:1;order:1}.order-sm-2{-ms-flex-order:2;order:2}.order-sm-3{-ms-flex-order:3;order:3}.order-sm-4{-ms-flex-order:4;order:4}.order-sm-5{-ms-flex-order:5;order:5}.order-sm-6{-ms-flex-order:6;order:6}.order-sm-7{-ms-flex-order:7;order:7}.order-sm-8{-ms-flex-order:8;order:8}.order-sm-9{-ms-flex-order:9;order:9}.order-sm-10{-ms-flex-order:10;order:10}.order-sm-11{-ms-flex-order:11;order:11}.order-sm-12{-ms-flex-order:12;order:12}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.333333%}.offset-sm-2{margin-left:16.666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.333333%}.offset-sm-5{margin-left:41.666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.333333%}.offset-sm-8{margin-left:66.666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.333333%}.offset-sm-11{margin-left:91.666667%}}@media (min-width:768px){.col-md{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-md-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-md-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-md-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-md-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-md-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-md-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-md-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-md-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-md-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-md-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-md-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-md-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-md-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-md-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-md-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-md-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-md-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-md-first{-ms-flex-order:-1;order:-1}.order-md-last{-ms-flex-order:13;order:13}.order-md-0{-ms-flex-order:0;order:0}.order-md-1{-ms-flex-order:1;order:1}.order-md-2{-ms-flex-order:2;order:2}.order-md-3{-ms-flex-order:3;order:3}.order-md-4{-ms-flex-order:4;order:4}.order-md-5{-ms-flex-order:5;order:5}.order-md-6{-ms-flex-order:6;order:6}.order-md-7{-ms-flex-order:7;order:7}.order-md-8{-ms-flex-order:8;order:8}.order-md-9{-ms-flex-order:9;order:9}.order-md-10{-ms-flex-order:10;order:10}.order-md-11{-ms-flex-order:11;order:11}.order-md-12{-ms-flex-order:12;order:12}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.333333%}.offset-md-2{margin-left:16.666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.333333%}.offset-md-5{margin-left:41.666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.333333%}.offset-md-8{margin-left:66.666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.333333%}.offset-md-11{margin-left:91.666667%}}@media (min-width:992px){.col-lg{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-lg-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-lg-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-lg-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-lg-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-lg-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-lg-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-lg-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-lg-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-lg-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-lg-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-lg-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-lg-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-lg-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-lg-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-lg-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-lg-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-lg-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-lg-first{-ms-flex-order:-1;order:-1}.order-lg-last{-ms-flex-order:13;order:13}.order-lg-0{-ms-flex-order:0;order:0}.order-lg-1{-ms-flex-order:1;order:1}.order-lg-2{-ms-flex-order:2;order:2}.order-lg-3{-ms-flex-order:3;order:3}.order-lg-4{-ms-flex-order:4;order:4}.order-lg-5{-ms-flex-order:5;order:5}.order-lg-6{-ms-flex-order:6;order:6}.order-lg-7{-ms-flex-order:7;order:7}.order-lg-8{-ms-flex-order:8;order:8}.order-lg-9{-ms-flex-order:9;order:9}.order-lg-10{-ms-flex-order:10;order:10}.order-lg-11{-ms-flex-order:11;order:11}.order-lg-12{-ms-flex-order:12;order:12}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.333333%}.offset-lg-2{margin-left:16.666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.333333%}.offset-lg-5{margin-left:41.666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.333333%}.offset-lg-8{margin-left:66.666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.333333%}.offset-lg-11{margin-left:91.666667%}}@media (min-width:1200px){.col-xl{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-xl-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-xl-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-xl-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-xl-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-xl-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-xl-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-xl-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-xl-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-xl-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-xl-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-xl-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-xl-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-xl-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-xl-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-xl-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-xl-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-xl-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-xl-first{-ms-flex-order:-1;order:-1}.order-xl-last{-ms-flex-order:13;order:13}.order-xl-0{-ms-flex-order:0;order:0}.order-xl-1{-ms-flex-order:1;order:1}.order-xl-2{-ms-flex-order:2;order:2}.order-xl-3{-ms-flex-order:3;order:3}.order-xl-4{-ms-flex-order:4;order:4}.order-xl-5{-ms-flex-order:5;order:5}.order-xl-6{-ms-flex-order:6;order:6}.order-xl-7{-ms-flex-order:7;order:7}.order-xl-8{-ms-flex-order:8;order:8}.order-xl-9{-ms-flex-order:9;order:9}.order-xl-10{-ms-flex-order:10;order:10}.order-xl-11{-ms-flex-order:11;order:11}.order-xl-12{-ms-flex-order:12;order:12}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.333333%}.offset-xl-2{margin-left:16.666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.333333%}.offset-xl-5{margin-left:41.666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.333333%}.offset-xl-8{margin-left:66.666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.333333%}.offset-xl-11{margin-left:91.666667%}}.table{width:100%;margin-bottom:1rem;color:#212529}.table td,.table th{padding:.75rem;vertical-align:top;border-top:1px solid #dee2e6}.table thead th{vertical-align:bottom;border-bottom:2px solid #dee2e6}.table tbody+tbody{border-top:2px solid #dee2e6}.table-sm td,.table-sm th{padding:.3rem}.table-bordered{border:1px solid #dee2e6}.table-bordered td,.table-bordered th{border:1px solid #dee2e6}.table-bordered thead td,.table-bordered thead th{border-bottom-width:2px}.table-borderless tbody+tbody,.table-borderless td,.table-borderless th,.table-borderless thead th{border:0}.table-striped tbody tr:nth-of-type(odd){background-color:rgba(0,0,0,.05)}.table-hover tbody tr:hover{color:#212529;background-color:rgba(0,0,0,.075)}.table-primary,.table-primary>td,.table-primary>th{background-color:#b8daff}.table-primary tbody+tbody,.table-primary td,.table-primary th,.table-primary thead th{border-color:#7abaff}.table-hover .table-primary:hover{background-color:#9fcdff}.table-hover .table-primary:hover>td,.table-hover .table-primary:hover>th{background-color:#9fcdff}.table-secondary,.table-secondary>td,.table-secondary>th{background-color:#d6d8db}.table-secondary tbody+tbody,.table-secondary td,.table-secondary th,.table-secondary thead th{border-color:#b3b7bb}.table-hover .table-secondary:hover{background-color:#c8cbcf}.table-hover .table-secondary:hover>td,.table-hover .table-secondary:hover>th{background-color:#c8cbcf}.table-success,.table-success>td,.table-success>th{background-color:#c3e6cb}.table-success tbody+tbody,.table-success td,.table-success th,.table-success thead th{border-color:#8fd19e}.table-hover .table-success:hover{background-color:#b1dfbb}.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#b1dfbb}.table-info,.table-info>td,.table-info>th{background-color:#bee5eb}.table-info tbody+tbody,.table-info td,.table-info th,.table-info thead th{border-color:#86cfda}.table-hover .table-info:hover{background-color:#abdde5}.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#abdde5}.table-warning,.table-warning>td,.table-warning>th{background-color:#ffeeba}.table-warning tbody+tbody,.table-warning td,.table-warning th,.table-warning thead th{border-color:#ffdf7e}.table-hover .table-warning:hover{background-color:#ffe8a1}.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#ffe8a1}.table-danger,.table-danger>td,.table-danger>th{background-color:#f5c6cb}.table-danger tbody+tbody,.table-danger td,.table-danger th,.table-danger thead th{border-color:#ed969e}.table-hover .table-danger:hover{background-color:#f1b0b7}.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#f1b0b7}.table-light,.table-light>td,.table-light>th{background-color:#fdfdfe}.table-light tbody+tbody,.table-light td,.table-light th,.table-light thead th{border-color:#fbfcfc}.table-hover .table-light:hover{background-color:#ececf6}.table-hover .table-light:hover>td,.table-hover .table-light:hover>th{background-color:#ececf6}.table-dark,.table-dark>td,.table-dark>th{background-color:#c6c8ca}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#95999c}.table-hover .table-dark:hover{background-color:#b9bbbe}.table-hover .table-dark:hover>td,.table-hover .table-dark:hover>th{background-color:#b9bbbe}.table-active,.table-active>td,.table-active>th{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:rgba(0,0,0,.075)}.table .thead-dark th{color:#fff;background-color:#343a40;border-color:#454d55}.table .thead-light th{color:#495057;background-color:#e9ecef;border-color:#dee2e6}.table-dark{color:#fff;background-color:#343a40}.table-dark td,.table-dark th,.table-dark thead th{border-color:#454d55}.table-dark.table-bordered{border:0}.table-dark.table-striped tbody tr:nth-of-type(odd){background-color:rgba(255,255,255,.05)}.table-dark.table-hover tbody tr:hover{color:#fff;background-color:rgba(255,255,255,.075)}@media (max-width:575.98px){.table-responsive-sm{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-sm>.table-bordered{border:0}}@media (max-width:767.98px){.table-responsive-md{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-md>.table-bordered{border:0}}@media (max-width:991.98px){.table-responsive-lg{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-lg>.table-bordered{border:0}}@media (max-width:1199.98px){.table-responsive-xl{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-xl>.table-bordered{border:0}}.table-responsive{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive>.table-bordered{border:0}.form-control{display:block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:focus{color:#495057;background-color:#fff;border-color:#80bdff;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.form-control::-webkit-input-placeholder{color:#6c757d;opacity:1}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control:-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}input[type=date].form-control,input[type=datetime-local].form-control,input[type=month].form-control,input[type=time].form-control{-webkit-appearance:none;-moz-appearance:none;appearance:none}select.form-control:-moz-focusring{color:transparent;text-shadow:0 0 0 #495057}select.form-control:focus::-ms-value{color:#495057;background-color:#fff}.form-control-file,.form-control-range{display:block;width:100%}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem;line-height:1.5}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem;line-height:1.5}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;font-size:1rem;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.form-control-lg{height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}select.form-control[multiple],select.form-control[size]{height:auto}textarea.form-control{height:auto}.form-group{margin-bottom:1rem}.form-text{display:block;margin-top:.25rem}.form-row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-5px;margin-left:-5px}.form-row>.col,.form-row>[class*=col-]{padding-right:5px;padding-left:5px}.form-check{position:relative;display:block;padding-left:1.25rem}.form-check-input{position:absolute;margin-top:.3rem;margin-left:-1.25rem}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{color:#6c757d}.form-check-label{margin-bottom:0}.form-check-inline{display:-ms-inline-flexbox;display:inline-flex;-ms-flex-align:center;align-items:center;padding-left:0;margin-right:.75rem}.form-check-inline .form-check-input{position:static;margin-top:0;margin-right:.3125rem;margin-left:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#28a745}.valid-tooltip{position:absolute;top:100%;left:0;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(40,167,69,.9);border-radius:.25rem}.form-row>.col>.valid-tooltip,.form-row>[class*=col-]>.valid-tooltip{left:5px}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:#28a745;padding-right:calc(1.5em + .75rem)!important;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.was-validated select.form-control:valid,select.form-control.is-valid{padding-right:3rem!important;background-position:right 1.5rem center}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.custom-select.is-valid,.was-validated .custom-select:valid{border-color:#28a745;padding-right:calc(.75em + 2.3125rem)!important;background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") right .75rem center/8px 10px no-repeat,#fff url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e") center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem) no-repeat}.custom-select.is-valid:focus,.was-validated .custom-select:valid:focus{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#28a745}.form-check-input.is-valid~.valid-feedback,.form-check-input.is-valid~.valid-tooltip,.was-validated .form-check-input:valid~.valid-feedback,.was-validated .form-check-input:valid~.valid-tooltip{display:block}.custom-control-input.is-valid~.custom-control-label,.was-validated .custom-control-input:valid~.custom-control-label{color:#28a745}.custom-control-input.is-valid~.custom-control-label::before,.was-validated .custom-control-input:valid~.custom-control-label::before{border-color:#28a745}.custom-control-input.is-valid:checked~.custom-control-label::before,.was-validated .custom-control-input:valid:checked~.custom-control-label::before{border-color:#34ce57;background-color:#34ce57}.custom-control-input.is-valid:focus~.custom-control-label::before,.was-validated .custom-control-input:valid:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.custom-control-input.is-valid:focus:not(:checked)~.custom-control-label::before,.was-validated .custom-control-input:valid:focus:not(:checked)~.custom-control-label::before{border-color:#28a745}.custom-file-input.is-valid~.custom-file-label,.was-validated .custom-file-input:valid~.custom-file-label{border-color:#28a745}.custom-file-input.is-valid:focus~.custom-file-label,.was-validated .custom-file-input:valid:focus~.custom-file-label{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;left:0;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(220,53,69,.9);border-radius:.25rem}.form-row>.col>.invalid-tooltip,.form-row>[class*=col-]>.invalid-tooltip{left:5px}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#dc3545;padding-right:calc(1.5em + .75rem)!important;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.was-validated select.form-control:invalid,select.form-control.is-invalid{padding-right:3rem!important;background-position:right 1.5rem center}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.custom-select.is-invalid,.was-validated .custom-select:invalid{border-color:#dc3545;padding-right:calc(.75em + 2.3125rem)!important;background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") right .75rem center/8px 10px no-repeat,#fff url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e") center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem) no-repeat}.custom-select.is-invalid:focus,.was-validated .custom-select:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-input.is-invalid~.invalid-feedback,.form-check-input.is-invalid~.invalid-tooltip,.was-validated .form-check-input:invalid~.invalid-feedback,.was-validated .form-check-input:invalid~.invalid-tooltip{display:block}.custom-control-input.is-invalid~.custom-control-label,.was-validated .custom-control-input:invalid~.custom-control-label{color:#dc3545}.custom-control-input.is-invalid~.custom-control-label::before,.was-validated .custom-control-input:invalid~.custom-control-label::before{border-color:#dc3545}.custom-control-input.is-invalid:checked~.custom-control-label::before,.was-validated .custom-control-input:invalid:checked~.custom-control-label::before{border-color:#e4606d;background-color:#e4606d}.custom-control-input.is-invalid:focus~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.custom-control-input.is-invalid:focus:not(:checked)~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus:not(:checked)~.custom-control-label::before{border-color:#dc3545}.custom-file-input.is-invalid~.custom-file-label,.was-validated .custom-file-input:invalid~.custom-file-label{border-color:#dc3545}.custom-file-input.is-invalid:focus~.custom-file-label,.was-validated .custom-file-input:invalid:focus~.custom-file-label{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.form-inline{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center}.form-inline .form-check{width:100%}@media (min-width:576px){.form-inline label{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;margin-bottom:0}.form-inline .form-group{display:-ms-flexbox;display:flex;-ms-flex:0 0 auto;flex:0 0 auto;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center;margin-bottom:0}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-plaintext{display:inline-block}.form-inline .custom-select,.form-inline .input-group{width:auto}.form-inline .form-check{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:auto;padding-left:0}.form-inline .form-check-input{position:relative;-ms-flex-negative:0;flex-shrink:0;margin-top:0;margin-right:.25rem;margin-left:0}.form-inline .custom-control{-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center}.form-inline .custom-control-label{margin-bottom:0}}.btn{display:inline-block;font-weight:400;color:#212529;text-align:center;vertical-align:middle;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;line-height:1.5;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:#212529;text-decoration:none}.btn.focus,.btn:focus{outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.btn.disabled,.btn:disabled{opacity:.65}.btn:not(:disabled):not(.disabled){cursor:pointer}a.btn.disabled,fieldset:disabled a.btn{pointer-events:none}.btn-primary{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:hover{color:#fff;background-color:#0069d9;border-color:#0062cc}.btn-primary.focus,.btn-primary:focus{color:#fff;background-color:#0069d9;border-color:#0062cc;box-shadow:0 0 0 .2rem rgba(38,143,255,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:not(:disabled):not(.disabled).active,.btn-primary:not(:disabled):not(.disabled):active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#0062cc;border-color:#005cbf}.btn-primary:not(:disabled):not(.disabled).active:focus,.btn-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(38,143,255,.5)}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{color:#fff;background-color:#5a6268;border-color:#545b62}.btn-secondary.focus,.btn-secondary:focus{color:#fff;background-color:#5a6268;border-color:#545b62;box-shadow:0 0 0 .2rem rgba(130,138,145,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:not(:disabled):not(.disabled).active,.btn-secondary:not(:disabled):not(.disabled):active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#545b62;border-color:#4e555b}.btn-secondary:not(:disabled):not(.disabled).active:focus,.btn-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(130,138,145,.5)}.btn-success{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:hover{color:#fff;background-color:#218838;border-color:#1e7e34}.btn-success.focus,.btn-success:focus{color:#fff;background-color:#218838;border-color:#1e7e34;box-shadow:0 0 0 .2rem rgba(72,180,97,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:not(:disabled):not(.disabled).active,.btn-success:not(:disabled):not(.disabled):active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#1e7e34;border-color:#1c7430}.btn-success:not(:disabled):not(.disabled).active:focus,.btn-success:not(:disabled):not(.disabled):active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(72,180,97,.5)}.btn-info{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:hover{color:#fff;background-color:#138496;border-color:#117a8b}.btn-info.focus,.btn-info:focus{color:#fff;background-color:#138496;border-color:#117a8b;box-shadow:0 0 0 .2rem rgba(58,176,195,.5)}.btn-info.disabled,.btn-info:disabled{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:not(:disabled):not(.disabled).active,.btn-info:not(:disabled):not(.disabled):active,.show>.btn-info.dropdown-toggle{color:#fff;background-color:#117a8b;border-color:#10707f}.btn-info:not(:disabled):not(.disabled).active:focus,.btn-info:not(:disabled):not(.disabled):active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(58,176,195,.5)}.btn-warning{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#212529;background-color:#e0a800;border-color:#d39e00}.btn-warning.focus,.btn-warning:focus{color:#212529;background-color:#e0a800;border-color:#d39e00;box-shadow:0 0 0 .2rem rgba(222,170,12,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:not(:disabled):not(.disabled).active,.btn-warning:not(:disabled):not(.disabled):active,.show>.btn-warning.dropdown-toggle{color:#212529;background-color:#d39e00;border-color:#c69500}.btn-warning:not(:disabled):not(.disabled).active:focus,.btn-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(222,170,12,.5)}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{color:#fff;background-color:#c82333;border-color:#bd2130}.btn-danger.focus,.btn-danger:focus{color:#fff;background-color:#c82333;border-color:#bd2130;box-shadow:0 0 0 .2rem rgba(225,83,97,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:not(:disabled):not(.disabled).active,.btn-danger:not(:disabled):not(.disabled):active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#bd2130;border-color:#b21f2d}.btn-danger:not(:disabled):not(.disabled).active:focus,.btn-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(225,83,97,.5)}.btn-light{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#212529;background-color:#e2e6ea;border-color:#dae0e5}.btn-light.focus,.btn-light:focus{color:#212529;background-color:#e2e6ea;border-color:#dae0e5;box-shadow:0 0 0 .2rem rgba(216,217,219,.5)}.btn-light.disabled,.btn-light:disabled{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:not(:disabled):not(.disabled).active,.btn-light:not(:disabled):not(.disabled):active,.show>.btn-light.dropdown-toggle{color:#212529;background-color:#dae0e5;border-color:#d3d9df}.btn-light:not(:disabled):not(.disabled).active:focus,.btn-light:not(:disabled):not(.disabled):active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(216,217,219,.5)}.btn-dark{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:hover{color:#fff;background-color:#23272b;border-color:#1d2124}.btn-dark.focus,.btn-dark:focus{color:#fff;background-color:#23272b;border-color:#1d2124;box-shadow:0 0 0 .2rem rgba(82,88,93,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:not(:disabled):not(.disabled).active,.btn-dark:not(:disabled):not(.disabled):active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1d2124;border-color:#171a1d}.btn-dark:not(:disabled):not(.disabled).active:focus,.btn-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(82,88,93,.5)}.btn-outline-primary{color:#007bff;border-color:#007bff}.btn-outline-primary:hover{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary.focus,.btn-outline-primary:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#007bff;background-color:transparent}.btn-outline-primary:not(:disabled):not(.disabled).active,.btn-outline-primary:not(:disabled):not(.disabled):active,.show>.btn-outline-primary.dropdown-toggle{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary:not(:disabled):not(.disabled).active:focus,.btn-outline-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-secondary{color:#6c757d;border-color:#6c757d}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary.focus,.btn-outline-secondary:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-secondary:not(:disabled):not(.disabled).active,.btn-outline-secondary:not(:disabled):not(.disabled):active,.show>.btn-outline-secondary.dropdown-toggle{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary:not(:disabled):not(.disabled).active:focus,.btn-outline-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-success{color:#28a745;border-color:#28a745}.btn-outline-success:hover{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success.focus,.btn-outline-success:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#28a745;background-color:transparent}.btn-outline-success:not(:disabled):not(.disabled).active,.btn-outline-success:not(:disabled):not(.disabled):active,.show>.btn-outline-success.dropdown-toggle{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success:not(:disabled):not(.disabled).active:focus,.btn-outline-success:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-info{color:#17a2b8;border-color:#17a2b8}.btn-outline-info:hover{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info.focus,.btn-outline-info:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#17a2b8;background-color:transparent}.btn-outline-info:not(:disabled):not(.disabled).active,.btn-outline-info:not(:disabled):not(.disabled):active,.show>.btn-outline-info.dropdown-toggle{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info:not(:disabled):not(.disabled).active:focus,.btn-outline-info:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-warning{color:#ffc107;border-color:#ffc107}.btn-outline-warning:hover{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning.focus,.btn-outline-warning:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-warning:not(:disabled):not(.disabled).active,.btn-outline-warning:not(:disabled):not(.disabled):active,.show>.btn-outline-warning.dropdown-toggle{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning:not(:disabled):not(.disabled).active:focus,.btn-outline-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-danger{color:#dc3545;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger.focus,.btn-outline-danger:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-danger:not(:disabled):not(.disabled).active,.btn-outline-danger:not(:disabled):not(.disabled):active,.show>.btn-outline-danger.dropdown-toggle{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger:not(:disabled):not(.disabled).active:focus,.btn-outline-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-light{color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:hover{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light.focus,.btn-outline-light:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-light:not(:disabled):not(.disabled).active,.btn-outline-light:not(:disabled):not(.disabled):active,.show>.btn-outline-light.dropdown-toggle{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:not(:disabled):not(.disabled).active:focus,.btn-outline-light:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-dark{color:#343a40;border-color:#343a40}.btn-outline-dark:hover{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark.focus,.btn-outline-dark:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#343a40;background-color:transparent}.btn-outline-dark:not(:disabled):not(.disabled).active,.btn-outline-dark:not(:disabled):not(.disabled):active,.show>.btn-outline-dark.dropdown-toggle{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark:not(:disabled):not(.disabled).active:focus,.btn-outline-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-link{font-weight:400;color:#007bff;text-decoration:none}.btn-link:hover{color:#0056b3;text-decoration:underline}.btn-link.focus,.btn-link:focus{text-decoration:underline}.btn-link.disabled,.btn-link:disabled{color:#6c757d;pointer-events:none}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:.5rem}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{position:relative;height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.dropdown,.dropleft,.dropright,.dropup{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:10rem;padding:.5rem 0;margin:.125rem 0 0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-menu-left{right:auto;left:0}.dropdown-menu-right{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-left{right:auto;left:0}.dropdown-menu-sm-right{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-left{right:auto;left:0}.dropdown-menu-md-right{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-left{right:auto;left:0}.dropdown-menu-lg-right{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-left{right:auto;left:0}.dropdown-menu-xl-right{right:0;left:auto}}.dropup .dropdown-menu{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-menu{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropright .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropright .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-toggle::after{vertical-align:0}.dropleft .dropdown-menu{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropleft .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropleft .dropdown-toggle::after{display:none}.dropleft .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropleft .dropdown-toggle:empty::after{margin-left:0}.dropleft .dropdown-toggle::before{vertical-align:0}.dropdown-menu[x-placement^=bottom],.dropdown-menu[x-placement^=left],.dropdown-menu[x-placement^=right],.dropdown-menu[x-placement^=top]{right:auto;bottom:auto}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid #e9ecef}.dropdown-item{display:block;width:100%;padding:.25rem 1.5rem;clear:both;font-weight:400;color:#212529;text-align:inherit;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#16181b;text-decoration:none;background-color:#e9ecef}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#007bff}.dropdown-item.disabled,.dropdown-item:disabled{color:#adb5bd;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1.5rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1.5rem;color:#212529}.btn-group,.btn-group-vertical{position:relative;display:-ms-inline-flexbox;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;-ms-flex:1 1 auto;flex:1 1 auto}.btn-group-vertical>.btn:hover,.btn-group>.btn:hover{z-index:1}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus{z-index:1}.btn-toolbar{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-pack:start;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropright .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropleft .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{-ms-flex-direction:column;flex-direction:column;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:center;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn:not(:first-child){border-top-left-radius:0;border-top-right-radius:0}.btn-group-toggle>.btn,.btn-group-toggle>.btn-group>.btn{margin-bottom:0}.btn-group-toggle>.btn input[type=checkbox],.btn-group-toggle>.btn input[type=radio],.btn-group-toggle>.btn-group>.btn input[type=checkbox],.btn-group-toggle>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:stretch;align-items:stretch;width:100%}.input-group>.custom-file,.input-group>.custom-select,.input-group>.form-control,.input-group>.form-control-plaintext{position:relative;-ms-flex:1 1 auto;flex:1 1 auto;width:1%;min-width:0;margin-bottom:0}.input-group>.custom-file+.custom-file,.input-group>.custom-file+.custom-select,.input-group>.custom-file+.form-control,.input-group>.custom-select+.custom-file,.input-group>.custom-select+.custom-select,.input-group>.custom-select+.form-control,.input-group>.form-control+.custom-file,.input-group>.form-control+.custom-select,.input-group>.form-control+.form-control,.input-group>.form-control-plaintext+.custom-file,.input-group>.form-control-plaintext+.custom-select,.input-group>.form-control-plaintext+.form-control{margin-left:-1px}.input-group>.custom-file .custom-file-input:focus~.custom-file-label,.input-group>.custom-select:focus,.input-group>.form-control:focus{z-index:3}.input-group>.custom-file .custom-file-input:focus{z-index:4}.input-group>.custom-select:not(:first-child),.input-group>.form-control:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.custom-file{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center}.input-group>.custom-file:not(:last-child) .custom-file-label,.input-group>.custom-file:not(:last-child) .custom-file-label::after{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-file:not(:first-child) .custom-file-label{border-top-left-radius:0;border-bottom-left-radius:0}.input-group:not(.has-validation)>.custom-file:not(:last-child) .custom-file-label,.input-group:not(.has-validation)>.custom-file:not(:last-child) .custom-file-label::after,.input-group:not(.has-validation)>.custom-select:not(:last-child),.input-group:not(.has-validation)>.form-control:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>.custom-file:nth-last-child(n+3) .custom-file-label,.input-group.has-validation>.custom-file:nth-last-child(n+3) .custom-file-label::after,.input-group.has-validation>.custom-select:nth-last-child(n+3),.input-group.has-validation>.form-control:nth-last-child(n+3){border-top-right-radius:0;border-bottom-right-radius:0}.input-group-append,.input-group-prepend{display:-ms-flexbox;display:flex}.input-group-append .btn,.input-group-prepend .btn{position:relative;z-index:2}.input-group-append .btn:focus,.input-group-prepend .btn:focus{z-index:3}.input-group-append .btn+.btn,.input-group-append .btn+.input-group-text,.input-group-append .input-group-text+.btn,.input-group-append .input-group-text+.input-group-text,.input-group-prepend .btn+.btn,.input-group-prepend .btn+.input-group-text,.input-group-prepend .input-group-text+.btn,.input-group-prepend .input-group-text+.input-group-text{margin-left:-1px}.input-group-prepend{margin-right:-1px}.input-group-append{margin-left:-1px}.input-group-text{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:.375rem .75rem;margin-bottom:0;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.25rem}.input-group-text input[type=checkbox],.input-group-text input[type=radio]{margin-top:0}.input-group-lg>.custom-select,.input-group-lg>.form-control:not(textarea){height:calc(1.5em + 1rem + 2px)}.input-group-lg>.custom-select,.input-group-lg>.form-control,.input-group-lg>.input-group-append>.btn,.input-group-lg>.input-group-append>.input-group-text,.input-group-lg>.input-group-prepend>.btn,.input-group-lg>.input-group-prepend>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.input-group-sm>.custom-select,.input-group-sm>.form-control:not(textarea){height:calc(1.5em + .5rem + 2px)}.input-group-sm>.custom-select,.input-group-sm>.form-control,.input-group-sm>.input-group-append>.btn,.input-group-sm>.input-group-append>.input-group-text,.input-group-sm>.input-group-prepend>.btn,.input-group-sm>.input-group-prepend>.input-group-text{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.input-group-lg>.custom-select,.input-group-sm>.custom-select{padding-right:1.75rem}.input-group.has-validation>.input-group-append:nth-last-child(n+3)>.btn,.input-group.has-validation>.input-group-append:nth-last-child(n+3)>.input-group-text,.input-group:not(.has-validation)>.input-group-append:not(:last-child)>.btn,.input-group:not(.has-validation)>.input-group-append:not(:last-child)>.input-group-text,.input-group>.input-group-append:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group>.input-group-append:last-child>.input-group-text:not(:last-child),.input-group>.input-group-prepend>.btn,.input-group>.input-group-prepend>.input-group-text{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.input-group-append>.btn,.input-group>.input-group-append>.input-group-text,.input-group>.input-group-prepend:first-child>.btn:not(:first-child),.input-group>.input-group-prepend:first-child>.input-group-text:not(:first-child),.input-group>.input-group-prepend:not(:first-child)>.btn,.input-group>.input-group-prepend:not(:first-child)>.input-group-text{border-top-left-radius:0;border-bottom-left-radius:0}.custom-control{position:relative;z-index:1;display:block;min-height:1.5rem;padding-left:1.5rem;-webkit-print-color-adjust:exact;color-adjust:exact}.custom-control-inline{display:-ms-inline-flexbox;display:inline-flex;margin-right:1rem}.custom-control-input{position:absolute;left:0;z-index:-1;width:1rem;height:1.25rem;opacity:0}.custom-control-input:checked~.custom-control-label::before{color:#fff;border-color:#007bff;background-color:#007bff}.custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-control-input:focus:not(:checked)~.custom-control-label::before{border-color:#80bdff}.custom-control-input:not(:disabled):active~.custom-control-label::before{color:#fff;background-color:#b3d7ff;border-color:#b3d7ff}.custom-control-input:disabled~.custom-control-label,.custom-control-input[disabled]~.custom-control-label{color:#6c757d}.custom-control-input:disabled~.custom-control-label::before,.custom-control-input[disabled]~.custom-control-label::before{background-color:#e9ecef}.custom-control-label{position:relative;margin-bottom:0;vertical-align:top}.custom-control-label::before{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;pointer-events:none;content:"";background-color:#fff;border:#adb5bd solid 1px}.custom-control-label::after{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;content:"";background:50%/50% 50% no-repeat}.custom-checkbox .custom-control-label::before{border-radius:.25rem}.custom-checkbox .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26l2.974 2.99L8 2.193z'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::before{border-color:#007bff;background-color:#007bff}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' viewBox='0 0 4 4'%3e%3cpath stroke='%23fff' d='M0 2h4'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-checkbox .custom-control-input:disabled:indeterminate~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-radio .custom-control-label::before{border-radius:50%}.custom-radio .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.custom-radio .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-switch{padding-left:2.25rem}.custom-switch .custom-control-label::before{left:-2.25rem;width:1.75rem;pointer-events:all;border-radius:.5rem}.custom-switch .custom-control-label::after{top:calc(.25rem + 2px);left:calc(-2.25rem + 2px);width:calc(1rem - 4px);height:calc(1rem - 4px);background-color:#adb5bd;border-radius:.5rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-transform .15s ease-in-out;transition:transform .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:transform .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-transform .15s ease-in-out}@media (prefers-reduced-motion:reduce){.custom-switch .custom-control-label::after{transition:none}}.custom-switch .custom-control-input:checked~.custom-control-label::after{background-color:#fff;-webkit-transform:translateX(.75rem);transform:translateX(.75rem)}.custom-switch .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-select{display:inline-block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem 1.75rem .375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;vertical-align:middle;background:#fff url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") right .75rem center/8px 10px no-repeat;border:1px solid #ced4da;border-radius:.25rem;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-select:focus{border-color:#80bdff;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-select:focus::-ms-value{color:#495057;background-color:#fff}.custom-select[multiple],.custom-select[size]:not([size="1"]){height:auto;padding-right:.75rem;background-image:none}.custom-select:disabled{color:#6c757d;background-color:#e9ecef}.custom-select::-ms-expand{display:none}.custom-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #495057}.custom-select-sm{height:calc(1.5em + .5rem + 2px);padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem}.custom-select-lg{height:calc(1.5em + 1rem + 2px);padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem}.custom-file{position:relative;display:inline-block;width:100%;height:calc(1.5em + .75rem + 2px);margin-bottom:0}.custom-file-input{position:relative;z-index:2;width:100%;height:calc(1.5em + .75rem + 2px);margin:0;overflow:hidden;opacity:0}.custom-file-input:focus~.custom-file-label{border-color:#80bdff;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-file-input:disabled~.custom-file-label,.custom-file-input[disabled]~.custom-file-label{background-color:#e9ecef}.custom-file-input:lang(en)~.custom-file-label::after{content:"Browse"}.custom-file-input~.custom-file-label[data-browse]::after{content:attr(data-browse)}.custom-file-label{position:absolute;top:0;right:0;left:0;z-index:1;height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;overflow:hidden;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;border:1px solid #ced4da;border-radius:.25rem}.custom-file-label::after{position:absolute;top:0;right:0;bottom:0;z-index:3;display:block;height:calc(1.5em + .75rem);padding:.375rem .75rem;line-height:1.5;color:#495057;content:"Browse";background-color:#e9ecef;border-left:inherit;border-radius:0 .25rem .25rem 0}.custom-range{width:100%;height:1.4rem;padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-range:focus{outline:0}.custom-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range::-moz-focus-outer{border:0}.custom-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#007bff;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.custom-range::-webkit-slider-thumb:active{background-color:#b3d7ff}.custom-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#007bff;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-moz-range-thumb{-moz-transition:none;transition:none}}.custom-range::-moz-range-thumb:active{background-color:#b3d7ff}.custom-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-ms-thumb{width:1rem;height:1rem;margin-top:0;margin-right:.2rem;margin-left:.2rem;background-color:#007bff;border:0;border-radius:1rem;-ms-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-ms-thumb{-ms-transition:none;transition:none}}.custom-range::-ms-thumb:active{background-color:#b3d7ff}.custom-range::-ms-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:transparent;border-color:transparent;border-width:.5rem}.custom-range::-ms-fill-lower{background-color:#dee2e6;border-radius:1rem}.custom-range::-ms-fill-upper{margin-right:15px;background-color:#dee2e6;border-radius:1rem}.custom-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.custom-range:disabled::-webkit-slider-runnable-track{cursor:default}.custom-range:disabled::-moz-range-thumb{background-color:#adb5bd}.custom-range:disabled::-moz-range-track{cursor:default}.custom-range:disabled::-ms-thumb{background-color:#adb5bd}.custom-control-label::before,.custom-file-label,.custom-select{transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.custom-control-label::before,.custom-file-label,.custom-select{transition:none}}.nav{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem}.nav-link:focus,.nav-link:hover{text-decoration:none}.nav-link.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-link{margin-bottom:-1px;border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e9ecef #e9ecef #dee2e6}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#007bff}.nav-fill .nav-item,.nav-fill>.nav-link{-ms-flex:1 1 auto;flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;text-align:center}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between;padding:.5rem 1rem}.navbar .container,.navbar .container-fluid,.navbar .container-lg,.navbar .container-md,.navbar .container-sm,.navbar .container-xl{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between}.navbar-brand{display:inline-block;padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;line-height:inherit;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-nav{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static;float:none}.navbar-text{display:inline-block;padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{-ms-flex-preferred-size:100%;flex-basis:100%;-ms-flex-positive:1;flex-grow:1;-ms-flex-align:center;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem}.navbar-toggler:focus,.navbar-toggler:hover{text-decoration:none}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;content:"";background:50%/100% 100% no-repeat}.navbar-nav-scroll{max-height:75vh;overflow-y:auto}@media (max-width:575.98px){.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid,.navbar-expand-sm>.container-lg,.navbar-expand-sm>.container-md,.navbar-expand-sm>.container-sm,.navbar-expand-sm>.container-xl{padding-right:0;padding-left:0}}@media (min-width:576px){.navbar-expand-sm{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-sm .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid,.navbar-expand-sm>.container-lg,.navbar-expand-sm>.container-md,.navbar-expand-sm>.container-sm,.navbar-expand-sm>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}}@media (max-width:767.98px){.navbar-expand-md>.container,.navbar-expand-md>.container-fluid,.navbar-expand-md>.container-lg,.navbar-expand-md>.container-md,.navbar-expand-md>.container-sm,.navbar-expand-md>.container-xl{padding-right:0;padding-left:0}}@media (min-width:768px){.navbar-expand-md{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-md .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md>.container,.navbar-expand-md>.container-fluid,.navbar-expand-md>.container-lg,.navbar-expand-md>.container-md,.navbar-expand-md>.container-sm,.navbar-expand-md>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}}@media (max-width:991.98px){.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid,.navbar-expand-lg>.container-lg,.navbar-expand-lg>.container-md,.navbar-expand-lg>.container-sm,.navbar-expand-lg>.container-xl{padding-right:0;padding-left:0}}@media (min-width:992px){.navbar-expand-lg{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-lg .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid,.navbar-expand-lg>.container-lg,.navbar-expand-lg>.container-md,.navbar-expand-lg>.container-sm,.navbar-expand-lg>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}}@media (max-width:1199.98px){.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid,.navbar-expand-xl>.container-lg,.navbar-expand-xl>.container-md,.navbar-expand-xl>.container-sm,.navbar-expand-xl>.container-xl{padding-right:0;padding-left:0}}@media (min-width:1200px){.navbar-expand-xl{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-xl .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid,.navbar-expand-xl>.container-lg,.navbar-expand-xl>.container-md,.navbar-expand-xl>.container-sm,.navbar-expand-xl>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}}.navbar-expand{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand>.container,.navbar-expand>.container-fluid,.navbar-expand>.container-lg,.navbar-expand>.container-md,.navbar-expand>.container-sm,.navbar-expand>.container-xl{padding-right:0;padding-left:0}.navbar-expand .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand>.container,.navbar-expand>.container-fluid,.navbar-expand>.container-lg,.navbar-expand>.container-md,.navbar-expand>.container-sm,.navbar-expand>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-light .navbar-brand{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.5)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .nav-link.show,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.5);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.5%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:rgba(0,0,0,.5)}.navbar-light .navbar-text a{color:rgba(0,0,0,.9)}.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.5)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .active>.nav-link,.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .nav-link.show,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.5);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.5%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:rgba(255,255,255,.5)}.navbar-dark .navbar-text a{color:#fff}.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{-ms-flex:1 1 auto;flex:1 1 auto;min-height:1px;padding:1.25rem}.card-title{margin-bottom:.75rem}.card-subtitle{margin-top:-.375rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card-header{padding:.75rem 1.25rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-footer{padding:.75rem 1.25rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.625rem;margin-bottom:-.75rem;margin-left:-.625rem;border-bottom:0}.card-header-pills{margin-right:-.625rem;margin-left:-.625rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1.25rem;border-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom,.card-img-top{-ms-flex-negative:0;flex-shrink:0;width:100%}.card-img,.card-img-top{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-deck .card{margin-bottom:15px}@media (min-width:576px){.card-deck{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap;margin-right:-15px;margin-left:-15px}.card-deck .card{-ms-flex:1 0 0%;flex:1 0 0%;margin-right:15px;margin-bottom:0;margin-left:15px}}.card-group>.card{margin-bottom:15px}@media (min-width:576px){.card-group{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap}.card-group>.card{-ms-flex:1 0 0%;flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.card-columns .card{margin-bottom:.75rem}@media (min-width:576px){.card-columns{-webkit-column-count:3;-moz-column-count:3;column-count:3;-webkit-column-gap:1.25rem;-moz-column-gap:1.25rem;column-gap:1.25rem;orphans:1;widows:1}.card-columns .card{display:inline-block;width:100%}}.accordion{overflow-anchor:none}.accordion>.card{overflow:hidden}.accordion>.card:not(:last-of-type){border-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.accordion>.card:not(:first-of-type){border-top-left-radius:0;border-top-right-radius:0}.accordion>.card>.card-header{border-radius:0;margin-bottom:-1px}.breadcrumb{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding:.75rem 1rem;margin-bottom:1rem;list-style:none;background-color:#e9ecef;border-radius:.25rem}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:.5rem;color:#6c757d;content:"/"}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:underline}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:none}.breadcrumb-item.active{color:#6c757d}.pagination{display:-ms-flexbox;display:flex;padding-left:0;list-style:none;border-radius:.25rem}.page-link{position:relative;display:block;padding:.5rem .75rem;margin-left:-1px;line-height:1.25;color:#007bff;background-color:#fff;border:1px solid #dee2e6}.page-link:hover{z-index:2;color:#0056b3;text-decoration:none;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:3;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.page-item:first-child .page-link{margin-left:0;border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.page-item.active .page-link{z-index:3;color:#fff;background-color:#007bff;border-color:#007bff}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;cursor:auto;background-color:#fff;border-color:#dee2e6}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem;line-height:1.5}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem;line-height:1.5}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.25em .4em;font-size:75%;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.badge{transition:none}}a.badge:focus,a.badge:hover{text-decoration:none}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.badge-pill{padding-right:.6em;padding-left:.6em;border-radius:10rem}.badge-primary{color:#fff;background-color:#007bff}a.badge-primary:focus,a.badge-primary:hover{color:#fff;background-color:#0062cc}a.badge-primary.focus,a.badge-primary:focus{outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.badge-secondary{color:#fff;background-color:#6c757d}a.badge-secondary:focus,a.badge-secondary:hover{color:#fff;background-color:#545b62}a.badge-secondary.focus,a.badge-secondary:focus{outline:0;box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.badge-success{color:#fff;background-color:#28a745}a.badge-success:focus,a.badge-success:hover{color:#fff;background-color:#1e7e34}a.badge-success.focus,a.badge-success:focus{outline:0;box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.badge-info{color:#fff;background-color:#17a2b8}a.badge-info:focus,a.badge-info:hover{color:#fff;background-color:#117a8b}a.badge-info.focus,a.badge-info:focus{outline:0;box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.badge-warning{color:#212529;background-color:#ffc107}a.badge-warning:focus,a.badge-warning:hover{color:#212529;background-color:#d39e00}a.badge-warning.focus,a.badge-warning:focus{outline:0;box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.badge-danger{color:#fff;background-color:#dc3545}a.badge-danger:focus,a.badge-danger:hover{color:#fff;background-color:#bd2130}a.badge-danger.focus,a.badge-danger:focus{outline:0;box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.badge-light{color:#212529;background-color:#f8f9fa}a.badge-light:focus,a.badge-light:hover{color:#212529;background-color:#dae0e5}a.badge-light.focus,a.badge-light:focus{outline:0;box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.badge-dark{color:#fff;background-color:#343a40}a.badge-dark:focus,a.badge-dark:hover{color:#fff;background-color:#1d2124}a.badge-dark.focus,a.badge-dark:focus{outline:0;box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#e9ecef;border-radius:.3rem}@media (min-width:576px){.jumbotron{padding:4rem 2rem}}.jumbotron-fluid{padding-right:0;padding-left:0;border-radius:0}.alert{position:relative;padding:.75rem 1.25rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:4rem}.alert-dismissible .close{position:absolute;top:0;right:0;z-index:2;padding:.75rem 1.25rem;color:inherit}.alert-primary{color:#004085;background-color:#cce5ff;border-color:#b8daff}.alert-primary hr{border-top-color:#9fcdff}.alert-primary .alert-link{color:#002752}.alert-secondary{color:#383d41;background-color:#e2e3e5;border-color:#d6d8db}.alert-secondary hr{border-top-color:#c8cbcf}.alert-secondary .alert-link{color:#202326}.alert-success{color:#155724;background-color:#d4edda;border-color:#c3e6cb}.alert-success hr{border-top-color:#b1dfbb}.alert-success .alert-link{color:#0b2e13}.alert-info{color:#0c5460;background-color:#d1ecf1;border-color:#bee5eb}.alert-info hr{border-top-color:#abdde5}.alert-info .alert-link{color:#062c33}.alert-warning{color:#856404;background-color:#fff3cd;border-color:#ffeeba}.alert-warning hr{border-top-color:#ffe8a1}.alert-warning .alert-link{color:#533f03}.alert-danger{color:#721c24;background-color:#f8d7da;border-color:#f5c6cb}.alert-danger hr{border-top-color:#f1b0b7}.alert-danger .alert-link{color:#491217}.alert-light{color:#818182;background-color:#fefefe;border-color:#fdfdfe}.alert-light hr{border-top-color:#ececf6}.alert-light .alert-link{color:#686868}.alert-dark{color:#1b1e21;background-color:#d6d8d9;border-color:#c6c8ca}.alert-dark hr{border-top-color:#b9bbbe}.alert-dark .alert-link{color:#040505}@-webkit-keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}.progress{display:-ms-flexbox;display:flex;height:1rem;overflow:hidden;line-height:0;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem}.progress-bar{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center;overflow:hidden;color:#fff;text-align:center;white-space:nowrap;background-color:#007bff;transition:width .6s ease}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:1s linear infinite progress-bar-stripes;animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion:reduce){.progress-bar-animated{-webkit-animation:none;animation:none}}.media{display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start}.media-body{-ms-flex:1;flex:1}.list-group{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:.25rem}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.75rem 1.25rem;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#007bff;border-color:#007bff}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:-1px;border-top-width:1px}.list-group-horizontal{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}@media (min-width:576px){.list-group-horizontal-sm{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:768px){.list-group-horizontal-md{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:992px){.list-group-horizontal-lg{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1200px){.list-group-horizontal-xl{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 1px}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{color:#004085;background-color:#b8daff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#004085;background-color:#9fcdff}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#004085;border-color:#004085}.list-group-item-secondary{color:#383d41;background-color:#d6d8db}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#383d41;background-color:#c8cbcf}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#383d41;border-color:#383d41}.list-group-item-success{color:#155724;background-color:#c3e6cb}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#155724;background-color:#b1dfbb}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#155724;border-color:#155724}.list-group-item-info{color:#0c5460;background-color:#bee5eb}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#0c5460;background-color:#abdde5}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#0c5460;border-color:#0c5460}.list-group-item-warning{color:#856404;background-color:#ffeeba}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#856404;background-color:#ffe8a1}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#856404;border-color:#856404}.list-group-item-danger{color:#721c24;background-color:#f5c6cb}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#721c24;background-color:#f1b0b7}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#721c24;border-color:#721c24}.list-group-item-light{color:#818182;background-color:#fdfdfe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#818182;background-color:#ececf6}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#818182;border-color:#818182}.list-group-item-dark{color:#1b1e21;background-color:#c6c8ca}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#1b1e21;background-color:#b9bbbe}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#1b1e21;border-color:#1b1e21}.close{float:right;font-size:1.5rem;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.5}.close:hover{color:#000;text-decoration:none}.close:not(:disabled):not(.disabled):focus,.close:not(:disabled):not(.disabled):hover{opacity:.75}button.close{padding:0;background-color:transparent;border:0}a.close.disabled{pointer-events:none}.toast{-ms-flex-preferred-size:350px;flex-basis:350px;max-width:350px;font-size:.875rem;background-color:rgba(255,255,255,.85);background-clip:padding-box;border:1px solid rgba(0,0,0,.1);box-shadow:0 .25rem .75rem rgba(0,0,0,.1);opacity:0;border-radius:.25rem}.toast:not(:last-child){margin-bottom:.75rem}.toast.showing{opacity:1}.toast.show{display:block;opacity:1}.toast.hide{display:none}.toast-header{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:.25rem .75rem;color:#6c757d;background-color:rgba(255,255,255,.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,.05);border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.toast-body{padding:.75rem}.modal-open{overflow:hidden}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal{position:fixed;top:0;left:0;z-index:1050;display:none;width:100%;height:100%;overflow:hidden;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:-webkit-transform .3s ease-out;transition:transform .3s ease-out;transition:transform .3s ease-out,-webkit-transform .3s ease-out;-webkit-transform:translate(0,-50px);transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{-webkit-transform:none;transform:none}.modal.modal-static .modal-dialog{-webkit-transform:scale(1.02);transform:scale(1.02)}.modal-dialog-scrollable{display:-ms-flexbox;display:flex;max-height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 1rem);overflow:hidden}.modal-dialog-scrollable .modal-footer,.modal-dialog-scrollable .modal-header{-ms-flex-negative:0;flex-shrink:0}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;min-height:calc(100% - 1rem)}.modal-dialog-centered::before{display:block;height:calc(100vh - 1rem);height:-webkit-min-content;height:-moz-min-content;height:min-content;content:""}.modal-dialog-centered.modal-dialog-scrollable{-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center;height:100%}.modal-dialog-centered.modal-dialog-scrollable .modal-content{max-height:none}.modal-dialog-centered.modal-dialog-scrollable::before{content:none}.modal-content{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:justify;justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #dee2e6;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.modal-header .close{padding:1rem 1rem;margin:-1rem -1rem -1rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem}.modal-footer{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:end;justify-content:flex-end;padding:.75rem;border-top:1px solid #dee2e6;border-bottom-right-radius:calc(.3rem - 1px);border-bottom-left-radius:calc(.3rem - 1px)}.modal-footer>*{margin:.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{max-height:calc(100% - 3.5rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-dialog-centered::before{height:calc(100vh - 3.5rem);height:-webkit-min-content;height:-moz-min-content;height:min-content}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width:1200px){.modal-xl{max-width:1140px}}.tooltip{position:absolute;z-index:1070;display:block;margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[x-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[x-placement^=top] .arrow,.bs-tooltip-top .arrow{bottom:0}.bs-tooltip-auto[x-placement^=top] .arrow::before,.bs-tooltip-top .arrow::before{top:0;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[x-placement^=right],.bs-tooltip-right{padding:0 .4rem}.bs-tooltip-auto[x-placement^=right] .arrow,.bs-tooltip-right .arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=right] .arrow::before,.bs-tooltip-right .arrow::before{right:0;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[x-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[x-placement^=bottom] .arrow,.bs-tooltip-bottom .arrow{top:0}.bs-tooltip-auto[x-placement^=bottom] .arrow::before,.bs-tooltip-bottom .arrow::before{bottom:0;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[x-placement^=left],.bs-tooltip-left{padding:0 .4rem}.bs-tooltip-auto[x-placement^=left] .arrow,.bs-tooltip-left .arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=left] .arrow::before,.bs-tooltip-left .arrow::before{left:0;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1060;display:block;max-width:276px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover .arrow{position:absolute;display:block;width:1rem;height:.5rem;margin:0 .3rem}.popover .arrow::after,.popover .arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[x-placement^=top],.bs-popover-top{margin-bottom:.5rem}.bs-popover-auto[x-placement^=top]>.arrow,.bs-popover-top>.arrow{bottom:calc(-.5rem - 1px)}.bs-popover-auto[x-placement^=top]>.arrow::before,.bs-popover-top>.arrow::before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=top]>.arrow::after,.bs-popover-top>.arrow::after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#fff}.bs-popover-auto[x-placement^=right],.bs-popover-right{margin-left:.5rem}.bs-popover-auto[x-placement^=right]>.arrow,.bs-popover-right>.arrow{left:calc(-.5rem - 1px);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=right]>.arrow::before,.bs-popover-right>.arrow::before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=right]>.arrow::after,.bs-popover-right>.arrow::after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#fff}.bs-popover-auto[x-placement^=bottom],.bs-popover-bottom{margin-top:.5rem}.bs-popover-auto[x-placement^=bottom]>.arrow,.bs-popover-bottom>.arrow{top:calc(-.5rem - 1px)}.bs-popover-auto[x-placement^=bottom]>.arrow::before,.bs-popover-bottom>.arrow::before{top:0;border-width:0 .5rem .5rem .5rem;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=bottom]>.arrow::after,.bs-popover-bottom>.arrow::after{top:1px;border-width:0 .5rem .5rem .5rem;border-bottom-color:#fff}.bs-popover-auto[x-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f7f7f7}.bs-popover-auto[x-placement^=left],.bs-popover-left{margin-right:.5rem}.bs-popover-auto[x-placement^=left]>.arrow,.bs-popover-left>.arrow{right:calc(-.5rem - 1px);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=left]>.arrow::before,.bs-popover-left>.arrow::before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=left]>.arrow::after,.bs-popover-left>.arrow::after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#fff}.popover-header{padding:.5rem .75rem;margin-bottom:0;font-size:1rem;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:.5rem .75rem;color:#212529}.carousel{position:relative}.carousel.pointer-event{-ms-touch-action:pan-y;touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:-webkit-transform .6s ease-in-out;transition:transform .6s ease-in-out;transition:transform .6s ease-in-out,-webkit-transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-right,.carousel-item-next:not(.carousel-item-left){-webkit-transform:translateX(100%);transform:translateX(100%)}.active.carousel-item-left,.carousel-item-prev:not(.carousel-item-right){-webkit-transform:translateX(-100%);transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;-webkit-transform:none;transform:none}.carousel-fade .carousel-item-next.carousel-item-left,.carousel-fade .carousel-item-prev.carousel-item-right,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:0 0;border:0;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:20px;height:20px;background:50%/100% 100% no-repeat}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M5.25 0l-4 4 4 4 1.5-1.5L4.25 4l2.5-2.5L5.25 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M2.75 0l-1.5 1.5L3.75 4l-2.5 2.5L2.75 8l4-4-4-4z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:15;display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;padding-left:0;margin-right:15%;margin-left:15%;list-style:none}.carousel-indicators li{box-sizing:content-box;-ms-flex:0 1 auto;flex:0 1 auto;width:30px;height:3px;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators li{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center}@-webkit-keyframes spinner-border{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes spinner-border{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;border:.25em solid currentColor;border-right-color:transparent;border-radius:50%;-webkit-animation:.75s linear infinite spinner-border;animation:.75s linear infinite spinner-border}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@-webkit-keyframes spinner-grow{0%{-webkit-transform:scale(0);transform:scale(0)}50%{opacity:1;-webkit-transform:none;transform:none}}@keyframes spinner-grow{0%{-webkit-transform:scale(0);transform:scale(0)}50%{opacity:1;-webkit-transform:none;transform:none}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;background-color:currentColor;border-radius:50%;opacity:0;-webkit-animation:.75s linear infinite spinner-grow;animation:.75s linear infinite spinner-grow}.spinner-grow-sm{width:1rem;height:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{-webkit-animation-duration:1.5s;animation-duration:1.5s}}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.bg-primary{background-color:#007bff!important}a.bg-primary:focus,a.bg-primary:hover,button.bg-primary:focus,button.bg-primary:hover{background-color:#0062cc!important}.bg-secondary{background-color:#6c757d!important}a.bg-secondary:focus,a.bg-secondary:hover,button.bg-secondary:focus,button.bg-secondary:hover{background-color:#545b62!important}.bg-success{background-color:#28a745!important}a.bg-success:focus,a.bg-success:hover,button.bg-success:focus,button.bg-success:hover{background-color:#1e7e34!important}.bg-info{background-color:#17a2b8!important}a.bg-info:focus,a.bg-info:hover,button.bg-info:focus,button.bg-info:hover{background-color:#117a8b!important}.bg-warning{background-color:#ffc107!important}a.bg-warning:focus,a.bg-warning:hover,button.bg-warning:focus,button.bg-warning:hover{background-color:#d39e00!important}.bg-danger{background-color:#dc3545!important}a.bg-danger:focus,a.bg-danger:hover,button.bg-danger:focus,button.bg-danger:hover{background-color:#bd2130!important}.bg-light{background-color:#f8f9fa!important}a.bg-light:focus,a.bg-light:hover,button.bg-light:focus,button.bg-light:hover{background-color:#dae0e5!important}.bg-dark{background-color:#343a40!important}a.bg-dark:focus,a.bg-dark:hover,button.bg-dark:focus,button.bg-dark:hover{background-color:#1d2124!important}.bg-white{background-color:#fff!important}.bg-transparent{background-color:transparent!important}.border{border:1px solid #dee2e6!important}.border-top{border-top:1px solid #dee2e6!important}.border-right{border-right:1px solid #dee2e6!important}.border-bottom{border-bottom:1px solid #dee2e6!important}.border-left{border-left:1px solid #dee2e6!important}.border-0{border:0!important}.border-top-0{border-top:0!important}.border-right-0{border-right:0!important}.border-bottom-0{border-bottom:0!important}.border-left-0{border-left:0!important}.border-primary{border-color:#007bff!important}.border-secondary{border-color:#6c757d!important}.border-success{border-color:#28a745!important}.border-info{border-color:#17a2b8!important}.border-warning{border-color:#ffc107!important}.border-danger{border-color:#dc3545!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#343a40!important}.border-white{border-color:#fff!important}.rounded-sm{border-radius:.2rem!important}.rounded{border-radius:.25rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-right{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-left{border-top-left-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-lg{border-radius:.3rem!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:50rem!important}.rounded-0{border-radius:0!important}.clearfix::after{display:block;clear:both;content:""}.d-none{display:none!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:-ms-flexbox!important;display:flex!important}.d-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}@media (min-width:576px){.d-sm-none{display:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:-ms-flexbox!important;display:flex!important}.d-sm-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:768px){.d-md-none{display:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:-ms-flexbox!important;display:flex!important}.d-md-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:992px){.d-lg-none{display:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:-ms-flexbox!important;display:flex!important}.d-lg-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:1200px){.d-xl-none{display:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:-ms-flexbox!important;display:flex!important}.d-xl-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media print{.d-print-none{display:none!important}.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:-ms-flexbox!important;display:flex!important}.d-print-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}.embed-responsive{position:relative;display:block;width:100%;padding:0;overflow:hidden}.embed-responsive::before{display:block;content:""}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-21by9::before{padding-top:42.857143%}.embed-responsive-16by9::before{padding-top:56.25%}.embed-responsive-4by3::before{padding-top:75%}.embed-responsive-1by1::before{padding-top:100%}.flex-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-center{-ms-flex-align:center!important;align-items:center!important}.align-items-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}@media (min-width:576px){.flex-sm-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-sm-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-sm-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-sm-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-sm-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-sm-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-sm-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-sm-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-sm-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-sm-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-sm-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-sm-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-sm-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-sm-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-sm-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-sm-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-sm-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-sm-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-sm-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-sm-center{-ms-flex-align:center!important;align-items:center!important}.align-items-sm-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-sm-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-sm-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-sm-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-sm-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-sm-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-sm-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-sm-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-sm-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-sm-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-sm-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-sm-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-sm-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-sm-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:768px){.flex-md-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-md-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-md-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-md-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-md-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-md-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-md-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-md-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-md-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-md-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-md-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-md-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-md-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-md-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-md-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-md-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-md-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-md-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-md-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-md-center{-ms-flex-align:center!important;align-items:center!important}.align-items-md-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-md-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-md-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-md-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-md-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-md-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-md-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-md-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-md-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-md-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-md-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-md-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-md-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-md-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:992px){.flex-lg-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-lg-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-lg-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-lg-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-lg-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-lg-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-lg-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-lg-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-lg-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-lg-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-lg-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-lg-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-lg-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-lg-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-lg-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-lg-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-lg-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-lg-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-lg-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-lg-center{-ms-flex-align:center!important;align-items:center!important}.align-items-lg-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-lg-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-lg-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-lg-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-lg-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-lg-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-lg-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-lg-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-lg-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-lg-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-lg-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-lg-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-lg-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-lg-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:1200px){.flex-xl-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-xl-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-xl-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-xl-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-xl-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-xl-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-xl-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-xl-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-xl-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-xl-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-xl-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-xl-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-xl-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-xl-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-xl-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-xl-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-xl-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-xl-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-xl-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-xl-center{-ms-flex-align:center!important;align-items:center!important}.align-items-xl-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-xl-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-xl-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-xl-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-xl-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-xl-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-xl-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-xl-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-xl-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-xl-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-xl-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-xl-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-xl-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-xl-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}.float-left{float:left!important}.float-right{float:right!important}.float-none{float:none!important}@media (min-width:576px){.float-sm-left{float:left!important}.float-sm-right{float:right!important}.float-sm-none{float:none!important}}@media (min-width:768px){.float-md-left{float:left!important}.float-md-right{float:right!important}.float-md-none{float:none!important}}@media (min-width:992px){.float-lg-left{float:left!important}.float-lg-right{float:right!important}.float-lg-none{float:none!important}}@media (min-width:1200px){.float-xl-left{float:left!important}.float-xl-right{float:right!important}.float-xl-none{float:none!important}}.user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;-ms-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;-ms-user-select:none!important;user-select:none!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}@supports ((position:-webkit-sticky) or (position:sticky)){.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;overflow:visible;clip:auto;white-space:normal}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mw-100{max-width:100%!important}.mh-100{max-height:100%!important}.min-vw-100{min-width:100vw!important}.min-vh-100{min-height:100vh!important}.vw-100{width:100vw!important}.vh-100{height:100vh!important}.m-0{margin:0!important}.mt-0,.my-0{margin-top:0!important}.mr-0,.mx-0{margin-right:0!important}.mb-0,.my-0{margin-bottom:0!important}.ml-0,.mx-0{margin-left:0!important}.m-1{margin:.25rem!important}.mt-1,.my-1{margin-top:.25rem!important}.mr-1,.mx-1{margin-right:.25rem!important}.mb-1,.my-1{margin-bottom:.25rem!important}.ml-1,.mx-1{margin-left:.25rem!important}.m-2{margin:.5rem!important}.mt-2,.my-2{margin-top:.5rem!important}.mr-2,.mx-2{margin-right:.5rem!important}.mb-2,.my-2{margin-bottom:.5rem!important}.ml-2,.mx-2{margin-left:.5rem!important}.m-3{margin:1rem!important}.mt-3,.my-3{margin-top:1rem!important}.mr-3,.mx-3{margin-right:1rem!important}.mb-3,.my-3{margin-bottom:1rem!important}.ml-3,.mx-3{margin-left:1rem!important}.m-4{margin:1.5rem!important}.mt-4,.my-4{margin-top:1.5rem!important}.mr-4,.mx-4{margin-right:1.5rem!important}.mb-4,.my-4{margin-bottom:1.5rem!important}.ml-4,.mx-4{margin-left:1.5rem!important}.m-5{margin:3rem!important}.mt-5,.my-5{margin-top:3rem!important}.mr-5,.mx-5{margin-right:3rem!important}.mb-5,.my-5{margin-bottom:3rem!important}.ml-5,.mx-5{margin-left:3rem!important}.p-0{padding:0!important}.pt-0,.py-0{padding-top:0!important}.pr-0,.px-0{padding-right:0!important}.pb-0,.py-0{padding-bottom:0!important}.pl-0,.px-0{padding-left:0!important}.p-1{padding:.25rem!important}.pt-1,.py-1{padding-top:.25rem!important}.pr-1,.px-1{padding-right:.25rem!important}.pb-1,.py-1{padding-bottom:.25rem!important}.pl-1,.px-1{padding-left:.25rem!important}.p-2{padding:.5rem!important}.pt-2,.py-2{padding-top:.5rem!important}.pr-2,.px-2{padding-right:.5rem!important}.pb-2,.py-2{padding-bottom:.5rem!important}.pl-2,.px-2{padding-left:.5rem!important}.p-3{padding:1rem!important}.pt-3,.py-3{padding-top:1rem!important}.pr-3,.px-3{padding-right:1rem!important}.pb-3,.py-3{padding-bottom:1rem!important}.pl-3,.px-3{padding-left:1rem!important}.p-4{padding:1.5rem!important}.pt-4,.py-4{padding-top:1.5rem!important}.pr-4,.px-4{padding-right:1.5rem!important}.pb-4,.py-4{padding-bottom:1.5rem!important}.pl-4,.px-4{padding-left:1.5rem!important}.p-5{padding:3rem!important}.pt-5,.py-5{padding-top:3rem!important}.pr-5,.px-5{padding-right:3rem!important}.pb-5,.py-5{padding-bottom:3rem!important}.pl-5,.px-5{padding-left:3rem!important}.m-n1{margin:-.25rem!important}.mt-n1,.my-n1{margin-top:-.25rem!important}.mr-n1,.mx-n1{margin-right:-.25rem!important}.mb-n1,.my-n1{margin-bottom:-.25rem!important}.ml-n1,.mx-n1{margin-left:-.25rem!important}.m-n2{margin:-.5rem!important}.mt-n2,.my-n2{margin-top:-.5rem!important}.mr-n2,.mx-n2{margin-right:-.5rem!important}.mb-n2,.my-n2{margin-bottom:-.5rem!important}.ml-n2,.mx-n2{margin-left:-.5rem!important}.m-n3{margin:-1rem!important}.mt-n3,.my-n3{margin-top:-1rem!important}.mr-n3,.mx-n3{margin-right:-1rem!important}.mb-n3,.my-n3{margin-bottom:-1rem!important}.ml-n3,.mx-n3{margin-left:-1rem!important}.m-n4{margin:-1.5rem!important}.mt-n4,.my-n4{margin-top:-1.5rem!important}.mr-n4,.mx-n4{margin-right:-1.5rem!important}.mb-n4,.my-n4{margin-bottom:-1.5rem!important}.ml-n4,.mx-n4{margin-left:-1.5rem!important}.m-n5{margin:-3rem!important}.mt-n5,.my-n5{margin-top:-3rem!important}.mr-n5,.mx-n5{margin-right:-3rem!important}.mb-n5,.my-n5{margin-bottom:-3rem!important}.ml-n5,.mx-n5{margin-left:-3rem!important}.m-auto{margin:auto!important}.mt-auto,.my-auto{margin-top:auto!important}.mr-auto,.mx-auto{margin-right:auto!important}.mb-auto,.my-auto{margin-bottom:auto!important}.ml-auto,.mx-auto{margin-left:auto!important}@media (min-width:576px){.m-sm-0{margin:0!important}.mt-sm-0,.my-sm-0{margin-top:0!important}.mr-sm-0,.mx-sm-0{margin-right:0!important}.mb-sm-0,.my-sm-0{margin-bottom:0!important}.ml-sm-0,.mx-sm-0{margin-left:0!important}.m-sm-1{margin:.25rem!important}.mt-sm-1,.my-sm-1{margin-top:.25rem!important}.mr-sm-1,.mx-sm-1{margin-right:.25rem!important}.mb-sm-1,.my-sm-1{margin-bottom:.25rem!important}.ml-sm-1,.mx-sm-1{margin-left:.25rem!important}.m-sm-2{margin:.5rem!important}.mt-sm-2,.my-sm-2{margin-top:.5rem!important}.mr-sm-2,.mx-sm-2{margin-right:.5rem!important}.mb-sm-2,.my-sm-2{margin-bottom:.5rem!important}.ml-sm-2,.mx-sm-2{margin-left:.5rem!important}.m-sm-3{margin:1rem!important}.mt-sm-3,.my-sm-3{margin-top:1rem!important}.mr-sm-3,.mx-sm-3{margin-right:1rem!important}.mb-sm-3,.my-sm-3{margin-bottom:1rem!important}.ml-sm-3,.mx-sm-3{margin-left:1rem!important}.m-sm-4{margin:1.5rem!important}.mt-sm-4,.my-sm-4{margin-top:1.5rem!important}.mr-sm-4,.mx-sm-4{margin-right:1.5rem!important}.mb-sm-4,.my-sm-4{margin-bottom:1.5rem!important}.ml-sm-4,.mx-sm-4{margin-left:1.5rem!important}.m-sm-5{margin:3rem!important}.mt-sm-5,.my-sm-5{margin-top:3rem!important}.mr-sm-5,.mx-sm-5{margin-right:3rem!important}.mb-sm-5,.my-sm-5{margin-bottom:3rem!important}.ml-sm-5,.mx-sm-5{margin-left:3rem!important}.p-sm-0{padding:0!important}.pt-sm-0,.py-sm-0{padding-top:0!important}.pr-sm-0,.px-sm-0{padding-right:0!important}.pb-sm-0,.py-sm-0{padding-bottom:0!important}.pl-sm-0,.px-sm-0{padding-left:0!important}.p-sm-1{padding:.25rem!important}.pt-sm-1,.py-sm-1{padding-top:.25rem!important}.pr-sm-1,.px-sm-1{padding-right:.25rem!important}.pb-sm-1,.py-sm-1{padding-bottom:.25rem!important}.pl-sm-1,.px-sm-1{padding-left:.25rem!important}.p-sm-2{padding:.5rem!important}.pt-sm-2,.py-sm-2{padding-top:.5rem!important}.pr-sm-2,.px-sm-2{padding-right:.5rem!important}.pb-sm-2,.py-sm-2{padding-bottom:.5rem!important}.pl-sm-2,.px-sm-2{padding-left:.5rem!important}.p-sm-3{padding:1rem!important}.pt-sm-3,.py-sm-3{padding-top:1rem!important}.pr-sm-3,.px-sm-3{padding-right:1rem!important}.pb-sm-3,.py-sm-3{padding-bottom:1rem!important}.pl-sm-3,.px-sm-3{padding-left:1rem!important}.p-sm-4{padding:1.5rem!important}.pt-sm-4,.py-sm-4{padding-top:1.5rem!important}.pr-sm-4,.px-sm-4{padding-right:1.5rem!important}.pb-sm-4,.py-sm-4{padding-bottom:1.5rem!important}.pl-sm-4,.px-sm-4{padding-left:1.5rem!important}.p-sm-5{padding:3rem!important}.pt-sm-5,.py-sm-5{padding-top:3rem!important}.pr-sm-5,.px-sm-5{padding-right:3rem!important}.pb-sm-5,.py-sm-5{padding-bottom:3rem!important}.pl-sm-5,.px-sm-5{padding-left:3rem!important}.m-sm-n1{margin:-.25rem!important}.mt-sm-n1,.my-sm-n1{margin-top:-.25rem!important}.mr-sm-n1,.mx-sm-n1{margin-right:-.25rem!important}.mb-sm-n1,.my-sm-n1{margin-bottom:-.25rem!important}.ml-sm-n1,.mx-sm-n1{margin-left:-.25rem!important}.m-sm-n2{margin:-.5rem!important}.mt-sm-n2,.my-sm-n2{margin-top:-.5rem!important}.mr-sm-n2,.mx-sm-n2{margin-right:-.5rem!important}.mb-sm-n2,.my-sm-n2{margin-bottom:-.5rem!important}.ml-sm-n2,.mx-sm-n2{margin-left:-.5rem!important}.m-sm-n3{margin:-1rem!important}.mt-sm-n3,.my-sm-n3{margin-top:-1rem!important}.mr-sm-n3,.mx-sm-n3{margin-right:-1rem!important}.mb-sm-n3,.my-sm-n3{margin-bottom:-1rem!important}.ml-sm-n3,.mx-sm-n3{margin-left:-1rem!important}.m-sm-n4{margin:-1.5rem!important}.mt-sm-n4,.my-sm-n4{margin-top:-1.5rem!important}.mr-sm-n4,.mx-sm-n4{margin-right:-1.5rem!important}.mb-sm-n4,.my-sm-n4{margin-bottom:-1.5rem!important}.ml-sm-n4,.mx-sm-n4{margin-left:-1.5rem!important}.m-sm-n5{margin:-3rem!important}.mt-sm-n5,.my-sm-n5{margin-top:-3rem!important}.mr-sm-n5,.mx-sm-n5{margin-right:-3rem!important}.mb-sm-n5,.my-sm-n5{margin-bottom:-3rem!important}.ml-sm-n5,.mx-sm-n5{margin-left:-3rem!important}.m-sm-auto{margin:auto!important}.mt-sm-auto,.my-sm-auto{margin-top:auto!important}.mr-sm-auto,.mx-sm-auto{margin-right:auto!important}.mb-sm-auto,.my-sm-auto{margin-bottom:auto!important}.ml-sm-auto,.mx-sm-auto{margin-left:auto!important}}@media (min-width:768px){.m-md-0{margin:0!important}.mt-md-0,.my-md-0{margin-top:0!important}.mr-md-0,.mx-md-0{margin-right:0!important}.mb-md-0,.my-md-0{margin-bottom:0!important}.ml-md-0,.mx-md-0{margin-left:0!important}.m-md-1{margin:.25rem!important}.mt-md-1,.my-md-1{margin-top:.25rem!important}.mr-md-1,.mx-md-1{margin-right:.25rem!important}.mb-md-1,.my-md-1{margin-bottom:.25rem!important}.ml-md-1,.mx-md-1{margin-left:.25rem!important}.m-md-2{margin:.5rem!important}.mt-md-2,.my-md-2{margin-top:.5rem!important}.mr-md-2,.mx-md-2{margin-right:.5rem!important}.mb-md-2,.my-md-2{margin-bottom:.5rem!important}.ml-md-2,.mx-md-2{margin-left:.5rem!important}.m-md-3{margin:1rem!important}.mt-md-3,.my-md-3{margin-top:1rem!important}.mr-md-3,.mx-md-3{margin-right:1rem!important}.mb-md-3,.my-md-3{margin-bottom:1rem!important}.ml-md-3,.mx-md-3{margin-left:1rem!important}.m-md-4{margin:1.5rem!important}.mt-md-4,.my-md-4{margin-top:1.5rem!important}.mr-md-4,.mx-md-4{margin-right:1.5rem!important}.mb-md-4,.my-md-4{margin-bottom:1.5rem!important}.ml-md-4,.mx-md-4{margin-left:1.5rem!important}.m-md-5{margin:3rem!important}.mt-md-5,.my-md-5{margin-top:3rem!important}.mr-md-5,.mx-md-5{margin-right:3rem!important}.mb-md-5,.my-md-5{margin-bottom:3rem!important}.ml-md-5,.mx-md-5{margin-left:3rem!important}.p-md-0{padding:0!important}.pt-md-0,.py-md-0{padding-top:0!important}.pr-md-0,.px-md-0{padding-right:0!important}.pb-md-0,.py-md-0{padding-bottom:0!important}.pl-md-0,.px-md-0{padding-left:0!important}.p-md-1{padding:.25rem!important}.pt-md-1,.py-md-1{padding-top:.25rem!important}.pr-md-1,.px-md-1{padding-right:.25rem!important}.pb-md-1,.py-md-1{padding-bottom:.25rem!important}.pl-md-1,.px-md-1{padding-left:.25rem!important}.p-md-2{padding:.5rem!important}.pt-md-2,.py-md-2{padding-top:.5rem!important}.pr-md-2,.px-md-2{padding-right:.5rem!important}.pb-md-2,.py-md-2{padding-bottom:.5rem!important}.pl-md-2,.px-md-2{padding-left:.5rem!important}.p-md-3{padding:1rem!important}.pt-md-3,.py-md-3{padding-top:1rem!important}.pr-md-3,.px-md-3{padding-right:1rem!important}.pb-md-3,.py-md-3{padding-bottom:1rem!important}.pl-md-3,.px-md-3{padding-left:1rem!important}.p-md-4{padding:1.5rem!important}.pt-md-4,.py-md-4{padding-top:1.5rem!important}.pr-md-4,.px-md-4{padding-right:1.5rem!important}.pb-md-4,.py-md-4{padding-bottom:1.5rem!important}.pl-md-4,.px-md-4{padding-left:1.5rem!important}.p-md-5{padding:3rem!important}.pt-md-5,.py-md-5{padding-top:3rem!important}.pr-md-5,.px-md-5{padding-right:3rem!important}.pb-md-5,.py-md-5{padding-bottom:3rem!important}.pl-md-5,.px-md-5{padding-left:3rem!important}.m-md-n1{margin:-.25rem!important}.mt-md-n1,.my-md-n1{margin-top:-.25rem!important}.mr-md-n1,.mx-md-n1{margin-right:-.25rem!important}.mb-md-n1,.my-md-n1{margin-bottom:-.25rem!important}.ml-md-n1,.mx-md-n1{margin-left:-.25rem!important}.m-md-n2{margin:-.5rem!important}.mt-md-n2,.my-md-n2{margin-top:-.5rem!important}.mr-md-n2,.mx-md-n2{margin-right:-.5rem!important}.mb-md-n2,.my-md-n2{margin-bottom:-.5rem!important}.ml-md-n2,.mx-md-n2{margin-left:-.5rem!important}.m-md-n3{margin:-1rem!important}.mt-md-n3,.my-md-n3{margin-top:-1rem!important}.mr-md-n3,.mx-md-n3{margin-right:-1rem!important}.mb-md-n3,.my-md-n3{margin-bottom:-1rem!important}.ml-md-n3,.mx-md-n3{margin-left:-1rem!important}.m-md-n4{margin:-1.5rem!important}.mt-md-n4,.my-md-n4{margin-top:-1.5rem!important}.mr-md-n4,.mx-md-n4{margin-right:-1.5rem!important}.mb-md-n4,.my-md-n4{margin-bottom:-1.5rem!important}.ml-md-n4,.mx-md-n4{margin-left:-1.5rem!important}.m-md-n5{margin:-3rem!important}.mt-md-n5,.my-md-n5{margin-top:-3rem!important}.mr-md-n5,.mx-md-n5{margin-right:-3rem!important}.mb-md-n5,.my-md-n5{margin-bottom:-3rem!important}.ml-md-n5,.mx-md-n5{margin-left:-3rem!important}.m-md-auto{margin:auto!important}.mt-md-auto,.my-md-auto{margin-top:auto!important}.mr-md-auto,.mx-md-auto{margin-right:auto!important}.mb-md-auto,.my-md-auto{margin-bottom:auto!important}.ml-md-auto,.mx-md-auto{margin-left:auto!important}}@media (min-width:992px){.m-lg-0{margin:0!important}.mt-lg-0,.my-lg-0{margin-top:0!important}.mr-lg-0,.mx-lg-0{margin-right:0!important}.mb-lg-0,.my-lg-0{margin-bottom:0!important}.ml-lg-0,.mx-lg-0{margin-left:0!important}.m-lg-1{margin:.25rem!important}.mt-lg-1,.my-lg-1{margin-top:.25rem!important}.mr-lg-1,.mx-lg-1{margin-right:.25rem!important}.mb-lg-1,.my-lg-1{margin-bottom:.25rem!important}.ml-lg-1,.mx-lg-1{margin-left:.25rem!important}.m-lg-2{margin:.5rem!important}.mt-lg-2,.my-lg-2{margin-top:.5rem!important}.mr-lg-2,.mx-lg-2{margin-right:.5rem!important}.mb-lg-2,.my-lg-2{margin-bottom:.5rem!important}.ml-lg-2,.mx-lg-2{margin-left:.5rem!important}.m-lg-3{margin:1rem!important}.mt-lg-3,.my-lg-3{margin-top:1rem!important}.mr-lg-3,.mx-lg-3{margin-right:1rem!important}.mb-lg-3,.my-lg-3{margin-bottom:1rem!important}.ml-lg-3,.mx-lg-3{margin-left:1rem!important}.m-lg-4{margin:1.5rem!important}.mt-lg-4,.my-lg-4{margin-top:1.5rem!important}.mr-lg-4,.mx-lg-4{margin-right:1.5rem!important}.mb-lg-4,.my-lg-4{margin-bottom:1.5rem!important}.ml-lg-4,.mx-lg-4{margin-left:1.5rem!important}.m-lg-5{margin:3rem!important}.mt-lg-5,.my-lg-5{margin-top:3rem!important}.mr-lg-5,.mx-lg-5{margin-right:3rem!important}.mb-lg-5,.my-lg-5{margin-bottom:3rem!important}.ml-lg-5,.mx-lg-5{margin-left:3rem!important}.p-lg-0{padding:0!important}.pt-lg-0,.py-lg-0{padding-top:0!important}.pr-lg-0,.px-lg-0{padding-right:0!important}.pb-lg-0,.py-lg-0{padding-bottom:0!important}.pl-lg-0,.px-lg-0{padding-left:0!important}.p-lg-1{padding:.25rem!important}.pt-lg-1,.py-lg-1{padding-top:.25rem!important}.pr-lg-1,.px-lg-1{padding-right:.25rem!important}.pb-lg-1,.py-lg-1{padding-bottom:.25rem!important}.pl-lg-1,.px-lg-1{padding-left:.25rem!important}.p-lg-2{padding:.5rem!important}.pt-lg-2,.py-lg-2{padding-top:.5rem!important}.pr-lg-2,.px-lg-2{padding-right:.5rem!important}.pb-lg-2,.py-lg-2{padding-bottom:.5rem!important}.pl-lg-2,.px-lg-2{padding-left:.5rem!important}.p-lg-3{padding:1rem!important}.pt-lg-3,.py-lg-3{padding-top:1rem!important}.pr-lg-3,.px-lg-3{padding-right:1rem!important}.pb-lg-3,.py-lg-3{padding-bottom:1rem!important}.pl-lg-3,.px-lg-3{padding-left:1rem!important}.p-lg-4{padding:1.5rem!important}.pt-lg-4,.py-lg-4{padding-top:1.5rem!important}.pr-lg-4,.px-lg-4{padding-right:1.5rem!important}.pb-lg-4,.py-lg-4{padding-bottom:1.5rem!important}.pl-lg-4,.px-lg-4{padding-left:1.5rem!important}.p-lg-5{padding:3rem!important}.pt-lg-5,.py-lg-5{padding-top:3rem!important}.pr-lg-5,.px-lg-5{padding-right:3rem!important}.pb-lg-5,.py-lg-5{padding-bottom:3rem!important}.pl-lg-5,.px-lg-5{padding-left:3rem!important}.m-lg-n1{margin:-.25rem!important}.mt-lg-n1,.my-lg-n1{margin-top:-.25rem!important}.mr-lg-n1,.mx-lg-n1{margin-right:-.25rem!important}.mb-lg-n1,.my-lg-n1{margin-bottom:-.25rem!important}.ml-lg-n1,.mx-lg-n1{margin-left:-.25rem!important}.m-lg-n2{margin:-.5rem!important}.mt-lg-n2,.my-lg-n2{margin-top:-.5rem!important}.mr-lg-n2,.mx-lg-n2{margin-right:-.5rem!important}.mb-lg-n2,.my-lg-n2{margin-bottom:-.5rem!important}.ml-lg-n2,.mx-lg-n2{margin-left:-.5rem!important}.m-lg-n3{margin:-1rem!important}.mt-lg-n3,.my-lg-n3{margin-top:-1rem!important}.mr-lg-n3,.mx-lg-n3{margin-right:-1rem!important}.mb-lg-n3,.my-lg-n3{margin-bottom:-1rem!important}.ml-lg-n3,.mx-lg-n3{margin-left:-1rem!important}.m-lg-n4{margin:-1.5rem!important}.mt-lg-n4,.my-lg-n4{margin-top:-1.5rem!important}.mr-lg-n4,.mx-lg-n4{margin-right:-1.5rem!important}.mb-lg-n4,.my-lg-n4{margin-bottom:-1.5rem!important}.ml-lg-n4,.mx-lg-n4{margin-left:-1.5rem!important}.m-lg-n5{margin:-3rem!important}.mt-lg-n5,.my-lg-n5{margin-top:-3rem!important}.mr-lg-n5,.mx-lg-n5{margin-right:-3rem!important}.mb-lg-n5,.my-lg-n5{margin-bottom:-3rem!important}.ml-lg-n5,.mx-lg-n5{margin-left:-3rem!important}.m-lg-auto{margin:auto!important}.mt-lg-auto,.my-lg-auto{margin-top:auto!important}.mr-lg-auto,.mx-lg-auto{margin-right:auto!important}.mb-lg-auto,.my-lg-auto{margin-bottom:auto!important}.ml-lg-auto,.mx-lg-auto{margin-left:auto!important}}@media (min-width:1200px){.m-xl-0{margin:0!important}.mt-xl-0,.my-xl-0{margin-top:0!important}.mr-xl-0,.mx-xl-0{margin-right:0!important}.mb-xl-0,.my-xl-0{margin-bottom:0!important}.ml-xl-0,.mx-xl-0{margin-left:0!important}.m-xl-1{margin:.25rem!important}.mt-xl-1,.my-xl-1{margin-top:.25rem!important}.mr-xl-1,.mx-xl-1{margin-right:.25rem!important}.mb-xl-1,.my-xl-1{margin-bottom:.25rem!important}.ml-xl-1,.mx-xl-1{margin-left:.25rem!important}.m-xl-2{margin:.5rem!important}.mt-xl-2,.my-xl-2{margin-top:.5rem!important}.mr-xl-2,.mx-xl-2{margin-right:.5rem!important}.mb-xl-2,.my-xl-2{margin-bottom:.5rem!important}.ml-xl-2,.mx-xl-2{margin-left:.5rem!important}.m-xl-3{margin:1rem!important}.mt-xl-3,.my-xl-3{margin-top:1rem!important}.mr-xl-3,.mx-xl-3{margin-right:1rem!important}.mb-xl-3,.my-xl-3{margin-bottom:1rem!important}.ml-xl-3,.mx-xl-3{margin-left:1rem!important}.m-xl-4{margin:1.5rem!important}.mt-xl-4,.my-xl-4{margin-top:1.5rem!important}.mr-xl-4,.mx-xl-4{margin-right:1.5rem!important}.mb-xl-4,.my-xl-4{margin-bottom:1.5rem!important}.ml-xl-4,.mx-xl-4{margin-left:1.5rem!important}.m-xl-5{margin:3rem!important}.mt-xl-5,.my-xl-5{margin-top:3rem!important}.mr-xl-5,.mx-xl-5{margin-right:3rem!important}.mb-xl-5,.my-xl-5{margin-bottom:3rem!important}.ml-xl-5,.mx-xl-5{margin-left:3rem!important}.p-xl-0{padding:0!important}.pt-xl-0,.py-xl-0{padding-top:0!important}.pr-xl-0,.px-xl-0{padding-right:0!important}.pb-xl-0,.py-xl-0{padding-bottom:0!important}.pl-xl-0,.px-xl-0{padding-left:0!important}.p-xl-1{padding:.25rem!important}.pt-xl-1,.py-xl-1{padding-top:.25rem!important}.pr-xl-1,.px-xl-1{padding-right:.25rem!important}.pb-xl-1,.py-xl-1{padding-bottom:.25rem!important}.pl-xl-1,.px-xl-1{padding-left:.25rem!important}.p-xl-2{padding:.5rem!important}.pt-xl-2,.py-xl-2{padding-top:.5rem!important}.pr-xl-2,.px-xl-2{padding-right:.5rem!important}.pb-xl-2,.py-xl-2{padding-bottom:.5rem!important}.pl-xl-2,.px-xl-2{padding-left:.5rem!important}.p-xl-3{padding:1rem!important}.pt-xl-3,.py-xl-3{padding-top:1rem!important}.pr-xl-3,.px-xl-3{padding-right:1rem!important}.pb-xl-3,.py-xl-3{padding-bottom:1rem!important}.pl-xl-3,.px-xl-3{padding-left:1rem!important}.p-xl-4{padding:1.5rem!important}.pt-xl-4,.py-xl-4{padding-top:1.5rem!important}.pr-xl-4,.px-xl-4{padding-right:1.5rem!important}.pb-xl-4,.py-xl-4{padding-bottom:1.5rem!important}.pl-xl-4,.px-xl-4{padding-left:1.5rem!important}.p-xl-5{padding:3rem!important}.pt-xl-5,.py-xl-5{padding-top:3rem!important}.pr-xl-5,.px-xl-5{padding-right:3rem!important}.pb-xl-5,.py-xl-5{padding-bottom:3rem!important}.pl-xl-5,.px-xl-5{padding-left:3rem!important}.m-xl-n1{margin:-.25rem!important}.mt-xl-n1,.my-xl-n1{margin-top:-.25rem!important}.mr-xl-n1,.mx-xl-n1{margin-right:-.25rem!important}.mb-xl-n1,.my-xl-n1{margin-bottom:-.25rem!important}.ml-xl-n1,.mx-xl-n1{margin-left:-.25rem!important}.m-xl-n2{margin:-.5rem!important}.mt-xl-n2,.my-xl-n2{margin-top:-.5rem!important}.mr-xl-n2,.mx-xl-n2{margin-right:-.5rem!important}.mb-xl-n2,.my-xl-n2{margin-bottom:-.5rem!important}.ml-xl-n2,.mx-xl-n2{margin-left:-.5rem!important}.m-xl-n3{margin:-1rem!important}.mt-xl-n3,.my-xl-n3{margin-top:-1rem!important}.mr-xl-n3,.mx-xl-n3{margin-right:-1rem!important}.mb-xl-n3,.my-xl-n3{margin-bottom:-1rem!important}.ml-xl-n3,.mx-xl-n3{margin-left:-1rem!important}.m-xl-n4{margin:-1.5rem!important}.mt-xl-n4,.my-xl-n4{margin-top:-1.5rem!important}.mr-xl-n4,.mx-xl-n4{margin-right:-1.5rem!important}.mb-xl-n4,.my-xl-n4{margin-bottom:-1.5rem!important}.ml-xl-n4,.mx-xl-n4{margin-left:-1.5rem!important}.m-xl-n5{margin:-3rem!important}.mt-xl-n5,.my-xl-n5{margin-top:-3rem!important}.mr-xl-n5,.mx-xl-n5{margin-right:-3rem!important}.mb-xl-n5,.my-xl-n5{margin-bottom:-3rem!important}.ml-xl-n5,.mx-xl-n5{margin-left:-3rem!important}.m-xl-auto{margin:auto!important}.mt-xl-auto,.my-xl-auto{margin-top:auto!important}.mr-xl-auto,.mx-xl-auto{margin-right:auto!important}.mb-xl-auto,.my-xl-auto{margin-bottom:auto!important}.ml-xl-auto,.mx-xl-auto{margin-left:auto!important}}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;pointer-events:auto;content:"";background-color:rgba(0,0,0,0)}.text-monospace{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace!important}.text-justify{text-align:justify!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-left{text-align:left!important}.text-right{text-align:right!important}.text-center{text-align:center!important}@media (min-width:576px){.text-sm-left{text-align:left!important}.text-sm-right{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.text-md-left{text-align:left!important}.text-md-right{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.text-lg-left{text-align:left!important}.text-lg-right{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.text-xl-left{text-align:left!important}.text-xl-right{text-align:right!important}.text-xl-center{text-align:center!important}}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.font-weight-light{font-weight:300!important}.font-weight-lighter{font-weight:lighter!important}.font-weight-normal{font-weight:400!important}.font-weight-bold{font-weight:700!important}.font-weight-bolder{font-weight:bolder!important}.font-italic{font-style:italic!important}.text-white{color:#fff!important}.text-primary{color:#007bff!important}a.text-primary:focus,a.text-primary:hover{color:#0056b3!important}.text-secondary{color:#6c757d!important}a.text-secondary:focus,a.text-secondary:hover{color:#494f54!important}.text-success{color:#28a745!important}a.text-success:focus,a.text-success:hover{color:#19692c!important}.text-info{color:#17a2b8!important}a.text-info:focus,a.text-info:hover{color:#0f6674!important}.text-warning{color:#ffc107!important}a.text-warning:focus,a.text-warning:hover{color:#ba8b00!important}.text-danger{color:#dc3545!important}a.text-danger:focus,a.text-danger:hover{color:#a71d2a!important}.text-light{color:#f8f9fa!important}a.text-light:focus,a.text-light:hover{color:#cbd3da!important}.text-dark{color:#343a40!important}a.text-dark:focus,a.text-dark:hover{color:#121416!important}.text-body{color:#212529!important}.text-muted{color:#6c757d!important}.text-black-50{color:rgba(0,0,0,.5)!important}.text-white-50{color:rgba(255,255,255,.5)!important}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.text-decoration-none{text-decoration:none!important}.text-break{word-break:break-word!important;word-wrap:break-word!important}.text-reset{color:inherit!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media print{*,::after,::before{text-shadow:none!important;box-shadow:none!important}a:not(.btn){text-decoration:underline}abbr[title]::after{content:" (" attr(title) ")"}pre{white-space:pre-wrap!important}blockquote,pre{border:1px solid #adb5bd;page-break-inside:avoid}img,tr{page-break-inside:avoid}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}@page{size:a3}body{min-width:992px!important}.container{min-width:992px!important}.navbar{display:none}.badge{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #dee2e6!important}.table-dark{color:inherit}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#dee2e6}.table .thead-dark th{color:inherit;border-color:#dee2e6}} diff --git a/blade-core-oauth2/src/main/resources/static/css/iofrm-style.css b/blade-core-oauth2/src/main/resources/static/css/iofrm-style.css new file mode 100644 index 0000000..73bb14f --- /dev/null +++ b/blade-core-oauth2/src/main/resources/static/css/iofrm-style.css @@ -0,0 +1,1887 @@ +/*------------------------------------------------------------------ + * Theme Name: iofrm - form templates + * Theme URI: http://www.brandio.io/envato/iofrm + * Author: Brandio + * Author URI: http://www.brandio.io/ + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + -------------------------------------------------------------------*/ + +@import url("https://fonts.googleapis.com/css?family=Lato:100,300,400,700,900"); + +/* ----------------------------------- + 1 - General Styles +------------------------------------*/ +*, body { + font-family: "Lato", sans-serif; + font-weight: 400; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; + -moz-osx-font-smoothing: grayscale; +} + +html, body { + height: 100%; +} + +.form-body { + background-color: #0093FF; + height: 100%; +} + +.form-body > .row { + position: relative; + margin-left: 0; + margin-right: 0; + height: 100%; +} + +.website-logo { + display: inline-block; + position: absolute; + z-index: 1000; + top: 50px; + left: 50px; + right: initial; + bottom: initial; +} + +.website-logo img { + width: 100px; +} + +.website-logo a { + display: inline-block; +} + +.website-logo .logo { + display: inline-block; + background-image: url("../images/logo-light.svg"); + background-size: contain; + background-repeat: no-repeat; +} + +.website-logo .logo img { + width: 150px; +} + +.website-logo .logo img.logo-size { + opacity: 0 !important; +} + +.website-logo-inside { + margin-bottom: 50px; +} + +.website-logo-inside img { + width: 100px; +} + +.website-logo-inside a { + display: inline-block; +} + +.website-logo-inside .logo { + display: inline-block; + background-image: url("../images/logo-light.svg"); + background-size: contain; + background-repeat: no-repeat; +} + +.website-logo-inside .logo img { + width: 100px; +} + +.website-logo-inside .logo img.logo-size { + opacity: 0 !important; +} + +.preview-body { + padding-top: 70px; + padding-bottom: 70px; + text-align: center; +} + +.preview-body .web-logo { + margin-bottom: 50px; +} + +.preview-body .web-logo img { + width: 130px; +} + +.preview-body .web-title { + font-size: 30px; + font-weight: 300; + color: #000; + line-height: 35px; + margin-bottom: 50px; +} + +.preview-body .img-link { + display: inline-block; + width: 100%; + margin: 20px 0; + padding: 0 5px; +} + +.preview-body .img-link img { + width: 100%; + border-radius: 15px; + -webkit-box-shadow: 0 0 5px rgba(160, 163, 165, 0.38); + box-shadow: 0 0 5px rgba(160, 163, 165, 0.38); + -webkit-transition: all 0.5s cubic-bezier(0.34, 1.61, 0.7, 1); + transition: all 0.5s cubic-bezier(0.34, 1.61, 0.7, 1); +} + +.preview-body .img-link:hover img, .preview-body .img-link:focus img { + -webkit-transform: scale(1.05); + -moz-transform: scale(1.05); + -ms-transform: scale(1.05); + transform: scale(1.05); + -webkit-box-shadow: 0 11px 19px rgba(160, 163, 165, 0.3); + box-shadow: 0 11px 19px rgba(160, 163, 165, 0.3); +} + +.img-holder { + display: inline-block; + position: absolute; + top: 0; + left: 0; + width: 550px; + min-height: 700px; + height: 100%; + overflow: hidden; + background-color: #000000; + padding: 60px; + text-align: center; + z-index: 999; +} + +.img-holder .info-holder { + position: relative; + top: 50%; + -webkit-transform: translateY(-50%); + -moz-transform: translateY(-50%); + -ms-transform: translateY(-50%); + transform: translateY(-50%); +} + +.img-holder .info-holder h3 { + display: inline-block; + color: #fff; + text-align: left; + font-size: 23px; + font-weight: 900; + margin-bottom: 30px; + width: 100%; + max-width: 378px; + padding-right: 30px; +} + +.img-holder .info-holder h2 { + display: inline-block; + color: #fff; + text-align: left; + font-size: 32px; + font-weight: 900; + margin-bottom: 30px; + width: 100%; + max-width: 378px; +} + +.img-holder .info-holder h2 span { + font-size: 32px; + font-weight: 900; + color: #FE4777; +} + +.img-holder .info-holder p { + display: inline-block; + color: #fff; + text-align: left; + font-size: 18px; + font-weight: 300; + line-height: 20px; + margin-bottom: 50px; + width: 100%; + max-width: 378px; + padding-right: 30px; +} + +.img-holder .info-holder img { + width: 100%; + max-width: 378px; +} + +.img-holder .info-holder img.md-size { + max-width: 290px; +} + +.img-holder .info-holder.simple-info h3 { + padding-right: 0; +} + +.img-holder .info-holder.simple-info p { + padding-right: 0; +} + +.img-holder .info-holder.simple-info img { + max-width: 160px; + margin-bottom: 50px; +} + +.img-holder .bg { + position: absolute; + opacity: 0.23; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-image: url("../images/img1.jpg"); + background-size: cover; + background-position: center; + z-index: -1; +} + +@media (min-height: 700px) { + .img-holder { + position: fixed; + } + + .website-logo { + position: fixed; + } +} + +.form-holder { + margin-left: 550px; + width: 100%; +} + +.form-holder .form-content { + position: relative; + text-align: center; + display: -webkit-box; + display: -moz-box; + display: -ms-flexbox; + display: -webkit-flex; + display: flex; + -webkit-justify-content: center; + justify-content: center; + -webkit-align-items: center; + align-items: center; + padding: 60px; + min-height: 100%; +} + +.form-holder .form-content ::-webkit-input-placeholder { + color: #526489; +} + +.form-holder .form-content :-moz-placeholder { + color: #526489; +} + +.form-holder .form-content ::-moz-placeholder { + color: #526489; +} + +.form-holder .form-content :-ms-input-placeholder { + color: #526489; +} + +.form-control:focus { + -webkit-box-shadow: none; + box-shadow: none; +} + +.form-control ::-webkit-input-placeholder { + color: #526489; +} + +.form-control :-moz-placeholder { + color: #526489; +} + +.form-control ::-moz-placeholder { + color: #526489; +} + +.form-control :-ms-input-placeholder { + color: #526489; +} + +.form-content { + position: relative; + background-color: #0093FF; +} + +.form-content .form-group { + color: #fff; + font-size: 15px; + font-weight: 300; +} + +.form-content .form-items { + display: inline-block; + width: 100%; + max-width: 380px; + text-align: left; + -webkit-transition: all 0.4s ease; + transition: all 0.4s ease; +} + +.form-content .form-icon { + text-align: center; + width: 100%; + line-height: 0; + margin-top: calc(-42px - 35px); + margin-bottom: 28px; +} + +.form-content .form-icon .icon-holder { + position: relative; + display: inline-block; + width: 85px; + height: 85px; + border-radius: 85px; + background-color: #4A77F7; + padding: 20px; +} + +.form-content .form-icon .icon-holder img { + position: absolute; + width: 50%; + top: 50%; + left: 50%; + margin-top: -23%; + margin-left: -25%; +} + +.form-content h3 { + color: #fff; + text-align: left; + font-size: 24px; + font-weight: 900; + margin-bottom: 10px; +} + +.form-content h3.form-title { + margin-bottom: 30px; +} + +.form-content h3.form-title-center { + margin-bottom: 30px; + text-align: center; + font-size: 22px; +} + +.form-content p { + color: #fff; + text-align: left; + font-size: 18px; + font-weight: 300; + line-height: 20px; + margin-bottom: 30px; +} + +.form-content p.form-subtitle { + font-size: 16px; + margin-bottom: 15px; +} + +.form-content small.error-message { + color: lightcoral; +} + +.form-content label { + color: #fff; + text-align: left; + font-size: 15px; + font-weight: 300; + line-height: 20px; + margin-bottom: 10px; +} + +.form-content .page-links { + margin-bottom: 34px; +} + +.form-content .page-links a { + position: relative; + display: inline-block; + text-decoration: none; + color: #fff; + font-weight: 300; + font-size: 15px; + margin-right: 20px; + -webkit-transition: all 0.3s ease; + transition: all 0.3s ease; +} + +.form-content .page-links a:last-child { + margin-right: 0; +} + +.form-content .page-links a:after { + position: absolute; + content: ""; + width: 100%; + height: 2px; + left: 0; + bottom: -10px; + background-color: rgba(255, 255, 255, 0.5); + -webkit-transition: all 0.3s ease; + transition: all 0.3s ease; +} + +.form-content .page-links a.active { + font-weight: 700; +} + +.form-content .page-links a.active:after { + background-color: #fff; +} + +.form-content .page-links a:hover:after, .form-content .page-links a:focus:after { + background-color: #fff; +} + +.form-content input, .form-content .dropdown-toggle.btn-default { + width: 100%; + padding: 9px 20px; + text-align: left; + border: 0; + outline: 0; + border-radius: 6px; + background-color: #fff; + font-size: 15px; + font-weight: 300; + color: #8D8D8D; + -webkit-transition: all 0.3s ease; + transition: all 0.3s ease; + margin-bottom: 14px; +} + +.form-content input:hover, .form-content input:focus, .form-content .dropdown-toggle.btn-default:hover, .form-content .dropdown-toggle.btn-default:focus { + border: 0; + background-color: #ebeff8; + color: #8D8D8D; +} + +.form-content textarea { + position: static !important; + width: 100%; + padding: 8px 20px; + border-radius: 6px; + text-align: left; + background-color: #fff; + border: 0; + font-size: 15px; + font-weight: 300; + color: #8D8D8D; + outline: none; + resize: none; + height: 120px; + -webkit-transition: none; + transition: none; + margin-bottom: 14px; +} + +.form-content textarea:hover, .form-content textarea:focus { + border: 0; + background-color: #ebeff8; + color: #8D8D8D; +} + +.form-content .custom-file { + margin-bottom: 14px; +} + +.form-content .custom-file-label { + position: absolute; + padding: 9px 44px 9px 20px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + text-align: left; + border: 0; + outline: 0; + border-radius: 6px; + background-color: #fff; + font-size: 15px; + font-weight: 300; + color: #8D8D8D; + outline: none !important; + -webkit-box-shadow: none !important; + box-shadow: none !important; + -webkit-transition: all 0.3s ease; + transition: all 0.3s ease; +} + +.form-content .custom-file-label:after { + content: "\f382" !important; + font-family: Font Awesome\ 5 Free; + font-style: normal; + font-weight: 600; + padding: 0.475rem 0.75rem 0.375rem; + color: #495057; + background-color: transparent; + border-left: 0; + border-radius: 0; + -webkit-transition: all 0.3s ease; + transition: all 0.3s ease; +} + +.form-content input[type=checkbox], .form-content input[type=radio] { + width: auto; +} + +.form-content input[type=checkbox]:not(:checked), .form-content input[type=checkbox]:checked, .form-content input[type=radio]:not(:checked), .form-content input[type=radio]:checked { + position: absolute; + left: -9999px; +} + +.form-content input[type=checkbox]:not(:checked) + label, .form-content input[type=checkbox]:checked + label, .form-content input[type=radio]:not(:checked) + label, .form-content input[type=radio]:checked + label { + position: relative; + padding-left: 23px; + cursor: pointer; + display: inline; + color: #fff; + font-size: 15px; + font-weight: 700; + margin-left: 0; + -webkit-transition: all 0.3s ease; + transition: all 0.3s ease; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.form-content input[type=checkbox]:checked + label, .form-content input[type=radio]:checked + label { + color: #fff; +} + +.form-content input[type=checkbox]:checked + label:before, .form-content input[type=radio]:checked + label:before { + content: ""; + position: absolute; + left: 0; + top: 2px; + width: 15px; + height: 15px; + background: #fff; + border-radius: 50px; + border: 0px solid #fff; + -webkit-transition: all 0.3s ease; + transition: all 0.3s ease; +} + +.form-content input[type=checkbox]:not(:checked) + label:before, .form-content input[type=radio]:not(:checked) + label:before { + content: ""; + position: absolute; + left: 0; + top: 2px; + width: 15px; + height: 15px; + background: transparent; + border-radius: 50px; + border: 2px solid #fff; + -webkit-transition: all 0.3s ease; + transition: all 0.3s ease; +} + +.form-content input[type=checkbox]:not(:checked) + label:after, .form-content input[type=radio]:not(:checked) + label:after { + opacity: 0; + -webkit-transform: scale(0); + -moz-transform: scale(0); + -ms-transform: scale(0); + transform: scale(0); +} + +.form-content input[type=checkbox]:disabled + label, .form-content input[type=radio]:disabled + label { + opacity: 0.6; +} + +.form-content input[type=checkbox]:checked + label:after, .form-content input[type=checkbox]:not(:checked) + label:after { + content: "\f00c"; + font-family: "Font Awesome 5 Free"; + font-style: normal; + font-weight: 600; + position: absolute; + top: 3px; + left: 3px; + font-size: 9px; + color: #0093FF; + line-height: 14px; + -webkit-transition: all 0.2s ease; + transition: all 0.2s ease; +} + +.form-content input[type=checkbox]:checked + label:before { + border-radius: 4px; +} + +.form-content input[type=checkbox]:not(:checked) + label:before { + border-radius: 4px; +} + +.form-content input[type=radio]:checked + label:after, .form-content input[type=radio]:not(:checked) + label:after { + content: ""; + position: absolute; + top: 7px; + left: 5px; + width: 5px; + height: 5px; + border-radius: 20px; + background-color: #0093FF; + -webkit-transition: all 0.2s ease; + transition: all 0.2s ease; +} + +.form-content .custom-options { + display: -webkit-box; + display: -moz-box; + display: -ms-flexbox; + display: -webkit-flex; + display: flex; +} + +.form-content .custom-options input[type=checkbox], .form-content .custom-options input[type=radio] { + width: auto; +} + +.form-content .custom-options input[type=checkbox]:not(:checked), .form-content .custom-options input[type=checkbox]:checked, .form-content .custom-options input[type=radio]:not(:checked), .form-content .custom-options input[type=radio]:checked { + position: absolute; + left: -9999px; +} + +.form-content .custom-options input[type=checkbox]:not(:checked) + label, .form-content .custom-options input[type=checkbox]:checked + label, .form-content .custom-options input[type=radio]:not(:checked) + label, .form-content .custom-options input[type=radio]:checked + label { + position: relative; + padding-left: 0; + cursor: pointer; + display: inline; + color: #606060; + background-color: #F7F7F7; + font-size: 13px; + font-weight: 400; + margin-left: 0; + border-radius: 5px; + padding: 4px 10px; + margin-right: 10px; + -webkit-transition: all 0.3s ease; + transition: all 0.3s ease; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + flex-grow: 1; + text-align: center; +} + +.form-content .custom-options input[type=checkbox]:not(:checked) + label:last-child, .form-content .custom-options input[type=checkbox]:checked + label:last-child, .form-content .custom-options input[type=radio]:not(:checked) + label:last-child, .form-content .custom-options input[type=radio]:checked + label:last-child { + margin-right: 0; +} + +.form-content .custom-options input[type=checkbox]:not(:checked) + label:after, .form-content .custom-options input[type=checkbox]:checked + label:after, .form-content .custom-options input[type=radio]:not(:checked) + label:after, .form-content .custom-options input[type=radio]:checked + label:after { + display: none; +} + +.form-content .custom-options input[type=checkbox]:checked + label, .form-content .custom-options input[type=radio]:checked + label { + color: #fff; + background-color: #57D38C; + font-weight: 400; + -webkit-box-shadow: 0 3px 8px rgba(74, 230, 142, 0.35); + box-shadow: 0 3px 8px rgba(74, 230, 142, 0.35); +} + +.form-content .custom-options input[type=checkbox]:checked + label:before, .form-content .custom-options input[type=radio]:checked + label:before { + display: none; +} + +.form-content .custom-options input[type=checkbox]:not(:checked) + label:before, .form-content .custom-options input[type=radio]:not(:checked) + label:before { + display: none; +} + +.form-content .custom-options input[type=checkbox]:not(:checked) + label:after, .form-content .custom-options input[type=radio]:not(:checked) + label:after { + display: none; +} + +.form-content .form-button { + margin-top: 30px; + margin-bottom: 25px; +} + +.form-content .form-button .lbtn { + border-radius: 6px; + border: 0; + padding: 6px 28px; + background-color: #fff; + width: 100%; + color: #cb3444; + font-size: 14px; + font-weight: 700; + text-decoration: none; + cursor: pointer; + margin-right: 10px; + outline: none; + -webkit-transition: all 0.3s ease; + transition: all 0.3s ease; + -webkit-box-shadow: 0 0 0 rgba(0, 0, 0, 0.16); + box-shadow: 0 0 0 rgba(0, 0, 0, 0.16); +} + +.form-content .form-button .lbtn.lbtn-full { + width: 100%; +} + +.form-content .form-button .lbtn:last-child { + margin-right: 0; +} + +.form-content .form-button .lbtn:hover, .form-content .form-button .lbtn:focus { + -webkit-box-shadow: 0 5px 6px rgba(0, 0, 0, 0.16); + box-shadow: 0 5px 6px rgba(0, 0, 0, 0.16); +} + +.form-content .form-button .lbtn.less-padding { + padding: 6px 15px !important; +} + +.form-content .form-button .lbtn.extra-padding { + font-size: 16px; + padding: 10px 32px; +} + +.form-content .form-button .ibtn { + border-radius: 6px; + border: 0; + padding: 6px 28px; + background-color: #fff; + width: 100%; + color: #29A4FF; + font-size: 14px; + font-weight: 700; + text-decoration: none; + cursor: pointer; + margin-right: 10px; + outline: none; + -webkit-transition: all 0.3s ease; + transition: all 0.3s ease; + -webkit-box-shadow: 0 0 0 rgba(0, 0, 0, 0.16); + box-shadow: 0 0 0 rgba(0, 0, 0, 0.16); +} + +.form-content .form-button .ibtn.ibtn-full { + width: 100%; +} + +.form-content .form-button .ibtn:last-child { + margin-right: 0; +} + +.form-content .form-button .ibtn:hover, .form-content .form-button .ibtn:focus { + -webkit-box-shadow: 0 5px 6px rgba(0, 0, 0, 0.16); + box-shadow: 0 5px 6px rgba(0, 0, 0, 0.16); +} + +.form-content .form-button .ibtn.less-padding { + padding: 6px 15px !important; +} + +.form-content .form-button .ibtn.extra-padding { + font-size: 16px; + padding: 10px 32px; +} + +.form-content .form-button a { + font-size: 13px; + font-weight: 700; + color: #fff; +} + +.form-content .form-button.full-width { + margin-top: 15px; +} + +.form-content .form-button.full-width .lbtn { + width: 100%; +} + +.form-content .form-button.full-width .ibtn { + width: 100%; +} + +.form-content .btn { + border-radius: 6px; + padding: 6px 28px; + font-size: 14px; + font-weight: 700; + margin-right: 10px; + border: 0; +} + +.form-content .btn.btn-light { + color: #B0C2D0; +} + +.form-content .btn.btn-light:hover, .form-content .btn.btn-light:focus { + color: #a0b6c6; +} + +.form-content .btn :last-child { + margin-right: 0; +} + +.form-content form { + margin-bottom: 30px; +} + +.other-links { + padding: 10px; + text-align: center; + position: fixed; + bottom: 0; +} + +.form-content .other-links span { + font-size: 12px; + font-weight: 300; + color: #fff; + margin-right: 20px; +} + +.form-content .other-links a { + font-size: 12px; + font-weight: 700; + color: #fff; + margin-right: 10px; +} + +.form-content .other-links a:last-child { + margin-right: 0; +} + +.form-content .other-links a i { + display: inline-block; + width: 25px; + height: 25px; + background-color: #000; + color: #fff; + border-radius: 25px; + text-align: center; + padding-top: 5px; + font-size: 15px; + margin: 0 5px; + -webkit-transition: all 0.3s ease; + transition: all 0.3s ease; +} + +.form-content .other-links a i[class*=fa-twitter] { + background-color: #00aced; +} + +.form-content .other-links a i[class*=fa-facebook] { + background-color: #3b5998; +} + +.form-content .other-links a i[class*=fa-youtube] { + background-color: #bb0000; +} + +.form-content .other-links a i[class*=fa-google] { + background-color: #dd4b39; +} + +.form-content .other-links a i[class*=fa-linkedin] { + background-color: #007bb6; +} + +.form-content .other-links a i[class*=fa-instagram] { + background-color: #517fa4; +} + +.form-content .other-links a i:hover, .form-content .other-links a i:focus { + opacity: 0.8; +} + +.form-content.form-sm input, .form-content.form-sm .dropdown-toggle.btn-default { + padding: 6px 16px; + margin-bottom: 10px; + font-size: 14px; +} + +.form-content.form-sm textarea { + padding: 6px 16px; + margin-bottom: 10px; + font-size: 14px; +} + +.form-content.form-sm .form-button .lbtn { + padding: 4px 28px; +} + + +.form-content.form-sm .form-button .ibtn { + padding: 4px 28px; +} + +.form-content.form-sm .btn { + padding: 4px 28px; +} + +.form-content .form-sent { + position: absolute; + text-align: center; + opacity: 0; + pointer-events: none; + z-index: 1; + -webkit-transform-origin: center center; + -moz-transform-origin: center center; + -ms-transform-origin: center center; + transform-origin: center center; + -webkit-transform: scale(0.7) translateX(200px); + -moz-transform: scale(0.7) translateX(200px); + -ms-transform: scale(0.7) translateX(200px); + transform: scale(0.7) translateX(200px); + -webkit-transition: all 0.4s ease; + transition: all 0.4s ease; +} + +.form-content .form-sent.show-it { + opacity: 1; + pointer-events: all; + z-index: 2; + -webkit-transform: scale(1) translateX(0); + -moz-transform: scale(1) translateX(0); + -ms-transform: scale(1) translateX(0); + transform: scale(1) translateX(0); +} + +.form-content .form-sent.show-it .tick-holder .tick-icon { + -webkit-animation: tick-anime3 0.7s cubic-bezier(0.34, 1.61, 0.7, 1) 0s forwards; + -moz-animation: tick-anime3 0.7s cubic-bezier(0.34, 1.61, 0.7, 1) 0s forwards; + -ms-animation: tick-anime3 0.7s cubic-bezier(0.34, 1.61, 0.7, 1) 0s forwards; + animation: tick-anime3 0.7s cubic-bezier(0.34, 1.61, 0.7, 1) 0s forwards; +} + +.form-content .form-sent.show-it .tick-holder .tick-icon:before { + -webkit-animation: tick-anime1 0.2s linear 0.2s forwards; + -moz-animation: tick-anime1 0.2s linear 0.2s forwards; + -ms-animation: tick-anime1 0.2s linear 0.2s forwards; + animation: tick-anime1 0.2s linear 0.2s forwards; +} + +.form-content .form-sent.show-it .tick-holder .tick-icon:after { + -webkit-animation: tick-anime2 0.4s ease 0.4s forwards; + -moz-animation: tick-anime2 0.4s ease 0.4s forwards; + -ms-animation: tick-anime2 0.4s ease 0.4s forwards; + animation: tick-anime2 0.4s ease 0.4s forwards; +} + +.form-content .form-sent .tick-holder { + text-align: center; + margin-bottom: 12px; +} + +.form-content .form-sent .tick-holder .tick-icon { + position: relative; + display: inline-block; + width: 40px; + height: 40px; + border-radius: 40px; + background-color: rgba(255, 255, 255, 0); + -webkit-transform: rotate(35deg) scale(2); + -moz-transform: rotate(35deg) scale(2); + -ms-transform: rotate(35deg) scale(2); + transform: rotate(35deg) scale(2); + -webkit-transform-origin: center center; + -moz-transform-origin: center center; + -ms-transform-origin: center center; + transform-origin: center center; +} + +.form-content .form-sent .tick-holder .tick-icon:before { + content: ""; + position: absolute; + background-color: #fff; + width: 10px; + height: 2px; + top: 28px; + left: 14px; + border-radius: 2px; + -webkit-transform-origin: left center; + -moz-transform-origin: left center; + -ms-transform-origin: left center; + transform-origin: left center; + -webkit-transform: scaleX(0); + -moz-transform: scaleX(0); + -ms-transform: scaleX(0); + transform: scaleX(0); +} + +.form-content .form-sent .tick-holder .tick-icon:after { + content: ""; + position: absolute; + background-color: #fff; + width: 2px; + height: 20px; + top: 9px; + left: 22px; + border-radius: 2px; + -webkit-transform-origin: center bottom; + -moz-transform-origin: center bottom; + -ms-transform-origin: center bottom; + transform-origin: center bottom; + -webkit-transform: scaleY(0); + -moz-transform: scaleY(0); + -ms-transform: scaleY(0); + transform: scaleY(0); +} + +.form-content .form-sent h3 { + text-align: center; + color: #fff; +} + +.form-content .form-sent p { + text-align: center; + color: #fff; + font-size: 15px; + opacity: 0.8; + margin-bottom: 20px; +} + +.form-content .form-sent .info-holder { + font-size: 12px; + font-weight: 700; + color: #fff; + border-top: 1px solid rgba(255, 255, 255, 0.5); + padding: 10px; + margin-top: 60px; +} + +.form-content .form-sent .info-holder span { + font-size: 12px; + font-weight: 700; + color: #fff; + opacity: 0.6; +} + +.form-content .form-sent .info-holder a { + font-size: 12px; + font-weight: 700; + color: #fff; + opacity: 0.9; +} + +.form-content .hide-it { + opacity: 0; + z-index: 1; + pointer-events: none; + -webkit-transform-origin: center center; + -moz-transform-origin: center center; + -ms-transform-origin: center center; + transform-origin: center center; + -webkit-transform: scale(0.7) translateX(-200px); + -moz-transform: scale(0.7) translateX(-200px); + -ms-transform: scale(0.7) translateX(-200px); + transform: scale(0.7) translateX(-200px); +} + +.form-content .row { + margin-right: -6px; + margin-left: -6px; +} + +.form-content .row.top-padding { + padding-top: 30px; +} + +.form-content .row.top-padding .form-button { + margin-top: 0; +} + +.form-content .row .col { + padding-right: 6px; + padding-left: 6px; +} + +.input-with-ccicon { + position: relative; + display: inline-block; + width: 100%; +} + +.input-with-ccicon #ccicon { + position: absolute; + right: 0.6rem; + top: 0.55rem; + font-size: 1.6rem; +} + +.input-with-ccicon #ccicon[class*=visa] { + color: #3744a2; +} + +.input-with-ccicon #ccicon[class*=amex] { + color: #1d8bd4; +} + +.input-with-ccicon #ccicon[class*=diners-club] { + color: #1d72d4; +} + +.input-with-ccicon #ccicon[class*=mastercard] { + color: #e42613; +} + +.input-with-ccicon #ccicon[class*=discover] { + color: #ef940b; +} + +.input-with-ccicon input { + padding-right: 45px; +} + +.nav-tabs { + border-bottom: 0; + margin-bottom: 2.2rem; +} + +.nav-tabs .nav-item .nav-link { + position: relative; + border: 0; + font-weight: 300; + padding: 0.5rem 0; + margin-right: 1.2rem; + text-align: center; + color: #000; + background-color: transparent; + -webkit-transition: all 0.3s ease; + transition: all 0.3s ease; +} + +.nav-tabs .nav-item .nav-link:before { + content: ""; + position: absolute; + left: 0; + bottom: -3px; + width: 100%; + height: 2px; + background-color: #DEDEDE; + -webkit-transition: all 0.3s ease; + transition: all 0.3s ease; +} + +.nav-tabs .nav-item .nav-link.active { + font-weight: 700; +} + +.nav-tabs .nav-item .nav-link.active:before { + height: 3px; + background-color: #57D38C; +} + +.form-subtitle { + font-size: 19px; + font-weight: 300; + color: #fff; + margin-bottom: 1rem; +} + +.inline-el-holder .inline-el { + display: inline-block; + margin-right: 1.3rem; +} + +.rad-with-details { + margin-bottom: 1rem; +} + +.rad-with-details .more-info { + color: #fff; + font-size: 14px; + font-weight: 300; + margin-top: 0.3rem; +} + +.separator { + border-top: 1px solid #C7C7C7; + margin-top: 1rem; + margin-bottom: 2rem; +} + +input.sm-content { + max-width: 110px; +} + +.form-body.on-top .website-logo { + position: absolute; +} + +.form-body.on-top .img-holder { + display: block; + position: relative; + width: 100%; + min-height: initial; + height: initial; + overflow: initial; + padding: 40px; +} + +.form-body.on-top .img-holder .info-holder.simple-info h3 { + margin-bottom: 16px; +} + +.form-body.on-top .img-holder .info-holder.simple-info p { + margin-bottom: 10px; +} + +.form-body.on-top .img-holder .info-holder.simple-info img { + margin-bottom: 20px; +} + +.form-body.on-top .form-holder { + margin-left: 0; +} + +@keyframes tick-anime1 { + 0% { + -webkit-transform: scaleX(0); + -moz-transform: scaleX(0); + -ms-transform: scaleX(0); + transform: scaleX(0); + } + + 100% { + -webkit-transform: scaleX(1); + -moz-transform: scaleX(1); + -ms-transform: scaleX(1); + transform: scaleX(1); + } +} + +@keyframes tick-anime2 { + 0% { + -webkit-transform: scaleY(0); + -moz-transform: scaleY(0); + -ms-transform: scaleY(0); + transform: scaleY(0); + } + + 100% { + -webkit-transform: scaleY(1); + -moz-transform: scaleY(1); + -ms-transform: scaleY(1); + transform: scaleY(1); + } +} + +@keyframes tick-anime3 { + 0% { + background-color: rgba(255, 255, 255, 0); + -webkit-transform: rotate(35deg) scale(2); + -moz-transform: rotate(35deg) scale(2); + -ms-transform: rotate(35deg) scale(2); + transform: rotate(35deg) scale(2); + } + + 100% { + background-color: rgba(255, 255, 255, 0.2); + -webkit-transform: rotate(45deg) scale(1); + -moz-transform: rotate(45deg) scale(1); + -ms-transform: rotate(45deg) scale(1); + transform: rotate(45deg) scale(1); + } +} + +@keyframes c-tick-anime3 { + 0% { + background-color: rgba(233, 253, 214, 0); + -webkit-transform: rotate(35deg) scale(2); + -moz-transform: rotate(35deg) scale(2); + -ms-transform: rotate(35deg) scale(2); + transform: rotate(35deg) scale(2); + } + + 100% { + background-color: #E9FDD6; + -webkit-transform: rotate(45deg) scale(1); + -moz-transform: rotate(45deg) scale(1); + -ms-transform: rotate(45deg) scale(1); + transform: rotate(45deg) scale(1); + } +} + +.alert { + position: relative; + padding: 6px 12px; + border: 1px solid #000; + color: #000000; + font-size: 13px; + font-weight: 700; +} + +.alert a, .alert a.alert-link { + font-weight: 700; + color: #000000; +} + +.alert p { + font-size: 13px; + font-weight: 700; + margin-bottom: 18px; +} + +.alert.alert-primary { + background-color: #e2f0ff; + border-color: #3a86d6; +} + +.alert.alert-primary hr { + border-top-color: #3a86d6; +} + +.alert.alert-secondary { + background-color: #f0f0f0; + border-color: #8e9396; +} + +.alert.alert-secondary hr { + border-top-color: #8e9396; +} + +.alert.alert-success { + background-color: #F7FFF0; + border-color: #8CCB57; +} + +.alert.alert-success hr { + border-top-color: #8CCB57; +} + +.alert.alert-danger { + background-color: #FFFAFA; + border-color: #F55050; +} + +.alert.alert-danger hr { + border-top-color: #F55050; +} + +.alert.alert-warning { + background-color: #fff8e1; + border-color: #f1cb4b; +} + +.alert.alert-warning hr { + border-top-color: #f1cb4b; +} + +.alert.alert-info { + background-color: #dcedf1; + border-color: #42bfdb; +} + +.alert.alert-info hr { + border-top-color: #42bfdb; +} + +.alert.alert-light { + background-color: #fefefe; + border-color: #a7a4a4; +} + +.alert.alert-light hr { + border-top-color: #a7a4a4; +} + +.alert.alert-dark { + background-color: #d6d8d9; + border-color: #525557; +} + +.alert.alert-dark hr { + border-top-color: #525557; +} + +.alert.with-icon { + padding-left: 32px; +} + +.alert.with-icon[class*=alert-]:before { + position: absolute; + font-family: "Font Awesome 5 Free"; + font-style: normal; + font-weight: 600; + top: 7px; + left: 7px; + width: 20px; + font-size: 12px; + text-align: center; +} + +.alert.with-icon.alert-primary:before { + content: "\f12a"; + color: #3a86d6; +} + +.alert.with-icon.alert-secondary:before { + content: "\f12a"; + color: #8e9396; +} + +.alert.with-icon.alert-success:before { + content: "\f00c"; + color: #8CCB57; +} + +.alert.with-icon.alert-danger:before { + content: "\f071"; + color: #F55050; +} + +.alert.with-icon.alert-warning:before { + content: "\f06a"; + color: #f1cb4b; +} + +.alert.with-icon.alert-info:before { + content: "\f129"; + color: #42bfdb; +} + +.alert.with-icon.alert-light:before { + content: "\f12a"; + color: #a7a4a4; +} + +.alert.with-icon.alert-dark:before { + content: "\f12a"; + color: #525557; +} + +.alert .close { + color: #727272; + font-size: 0.9rem; + padding: 3px; + outline: none; +} + +.alert .close span { + color: #727272; +} + +.form-body.without-side .website-logo { + top: 70px; + left: 50%; + margin-left: -50px; + right: initial; + bottom: initial; + display: inline-block; +} + +.form-body.without-side .website-logo-inside .logo { + background-image: url("../images/logo-dark.svg"); +} + +.form-body.without-side .form-holder .form-content ::-webkit-input-placeholder { + color: #000; +} + +.form-body.without-side .form-holder .form-content :-moz-placeholder { + color: #000; +} + +.form-body.without-side .form-holder .form-content ::-moz-placeholder { + color: #000; +} + +.form-body.without-side .form-holder .form-content :-ms-input-placeholder { + color: #000; +} + +.form-body.without-side h3 { + color: #000; +} + +.form-body.without-side p { + color: #000; +} + +.form-body.without-side label { + color: #000; +} + +.form-body.without-side .img-holder { + z-index: 0; + width: 100%; + overflow: hidden; +} + +.form-body.without-side .img-holder .info-holder img { + display: none; + max-width: 534px; + -webkit-animation: zoom-in-img 50s linear 0s infinite; + -moz-animation: zoom-in-img 50s linear 0s infinite; + -ms-animation: zoom-in-img 50s linear 0s infinite; + animation: zoom-in-img 50s linear 0s infinite; + -webkit-transform-origin: center center; + -moz-transform-origin: center center; + -ms-transform-origin: center center; + transform-origin: center center; + -webkit-transform: scale(1); + -moz-transform: scale(1); + -ms-transform: scale(1); + transform: scale(1); +} + +.form-body.without-side .form-holder { + margin-left: 0; +} + +.form-body.without-side .form-holder .form-content { + background-color: transparent; +} + +.form-body.without-side .form-content { + padding: 125px 60px 60px; + -webkit-perspective: 800px; + -moz-perspective: 800px; + -ms-perspective: 800px; + perspective: 800px; +} + +.form-body.without-side .form-content .form-items { + padding: 35px 30px; + border-radius: 10px; + background-color: #fff; + -webkit-box-shadow: 0 6px 15px rgba(0, 0, 0, 0.16); + box-shadow: 0 6px 15px rgba(0, 0, 0, 0.16); +} + +.form-body.without-side .form-content .form-items form { + margin-bottom: 0; +} + +.form-body.without-side .form-content .form-items .other-links { + margin-top: 38px; + margin-bottom: 30px; +} + +.form-body.without-side .form-content .form-items .other-links .text { + font-size: 13px; + font-weight: 300; + color: #000; + margin-bottom: 15px; +} + +.form-body.without-side .form-content .form-items .other-links a { + display: inline-block; + padding: 5px; + border-radius: 2px; + color: #000; + background-color: #F7F7F7; +} + +.form-body.without-side .form-content .form-items .other-links a i { + width: 18px; + height: 18px; + font-size: 9px; + margin-left: 0; +} + +.form-body.without-side .form-content .form-items .page-links { + margin-bottom: 0; +} + +.form-body.without-side .form-content .form-items .page-links a { + font-weight: 700; +} + +.form-body.without-side .form-content .form-items .page-links a:after { + bottom: -3px; +} + +.form-body.without-side .form-content .page-links a { + color: #000; +} + +.form-body.without-side .form-content .page-links a:after { + background-color: rgba(222, 222, 222, 0.7); +} + +.form-body.without-side .form-content .page-links a.active:after, .form-body.without-side .form-content .page-links a:hover:after, .form-body.without-side .form-content .page-links a:focus:after { + background-color: #0092FE; +} + +.form-body.without-side .form-content input, .form-body.without-side .form-content .dropdown-toggle.btn-default { + border: 0; + background-color: #F7F7F7; + color: #000000; +} + +.form-body.without-side .form-content input:hover, .form-body.without-side .form-content input:focus, .form-body.without-side .form-content .dropdown-toggle.btn-default:hover, .form-body.without-side .form-content .dropdown-toggle.btn-default:focus { + border: 0; + background-color: #eaeaea; + color: #000000; +} + +.form-body.without-side .form-content textarea { + background-color: #F7F7F7; + border: 0; + color: #000000; +} + +.form-body.without-side .form-content textarea:hover, .form-body.without-side .form-content textarea:focus { + border: 0; + background-color: #eaeaea; + color: #000000; +} + +.form-body.without-side .form-content input[type=checkbox]:not(:checked) + label, .form-body.without-side .form-content input[type=checkbox]:checked + label, .form-body.without-side .form-content input[type=radio]:not(:checked) + label, .form-body.without-side .form-content input[type=radio]:checked + label { + color: #000; +} + +.form-body.without-side .form-content input[type=checkbox]:checked + label, .form-body.without-side .form-content input[type=radio]:checked + label { + color: #000; +} + +.form-body.without-side .form-content input[type=checkbox]:checked + label:before, .form-body.without-side .form-content input[type=radio]:checked + label:before { + background: #000; + border: 0px solid #000; +} + +.form-body.without-side .form-content input[type=checkbox]:not(:checked) + label:before, .form-body.without-side .form-content input[type=radio]:not(:checked) + label:before { + background: transparent; + border: 2px solid #000; +} + +.form-body.without-side .form-content input[type=checkbox]:not(:checked) + label:after, .form-body.without-side .form-content input[type=checkbox]:checked + label:after { + color: #fff; +} + +.form-body.without-side .form-content input[type=radio]:not(:checked) + label:after, .form-body.without-side .form-content input[type=radio]:checked + label:after { + background-color: #fff; +} + +.form-body.without-side .form-content .custom-options input[type=checkbox]:not(:checked) + label, .form-body.without-side .form-content .custom-options input[type=checkbox]:checked + label, .form-body.without-side .form-content .custom-options input[type=radio]:not(:checked) + label, .form-body.without-side .form-content .custom-options input[type=radio]:checked + label { + color: #606060; + background-color: #F7F7F7; +} + +.form-body.without-side .form-content .custom-options input[type=checkbox]:checked + label, .form-body.without-side .form-content .custom-options input[type=radio]:checked + label { + color: #fff; + background-color: #57D38C; + -webkit-box-shadow: 0 3px 8px rgba(74, 230, 142, 0.35); + box-shadow: 0 3px 8px rgba(74, 230, 142, 0.35); +} + +.form-body.without-side .form-content .form-button { + margin-bottom: 0; +} + +.form-body.without-side .form-content .form-button .lbtn { + background-color: #cb3444; + color: #fff; + -webkit-box-shadow: 0 0 0 rgba(0, 0, 0, 0.16); + box-shadow: 0 0 0 rgba(0, 0, 0, 0.16); +} + +.form-body.without-side .form-content .form-button .lbtn:hover, .form-body.without-side .form-content .form-button .lbtn:focus { + -webkit-box-shadow: 0 5px 6px rgba(0, 0, 0, 0.16); + box-shadow: 0 5px 6px rgba(0, 0, 0, 0.16); +} + +.form-body.without-side .form-content .form-button .ibtn { + background-color: #29A4FF; + color: #fff; + -webkit-box-shadow: 0 0 0 rgba(0, 0, 0, 0.16); + box-shadow: 0 0 0 rgba(0, 0, 0, 0.16); +} + +.form-body.without-side .form-content .form-button .ibtn:hover, .form-body.without-side .form-content .form-button .ibtn:focus { + -webkit-box-shadow: 0 5px 6px rgba(0, 0, 0, 0.16); + box-shadow: 0 5px 6px rgba(0, 0, 0, 0.16); +} + +.form-body.without-side .form-content .form-button a { + font-weight: 300; + color: #000; +} + +.form-body.without-side .form-content .form-sent { + padding: 35px 30px; + border-radius: 10px; + background-color: #fff; + -webkit-box-shadow: 0 6px 15px rgba(0, 0, 0, 0.16); + box-shadow: 0 6px 15px rgba(0, 0, 0, 0.16); + -webkit-transform: rotateY(-180deg); + -moz-transform: rotateY(-180deg); + -ms-transform: rotateY(-180deg); + transform: rotateY(-180deg); +} + +.form-body.without-side .form-content .form-sent.show-it { + -webkit-transform: rotateY(0deg); + -moz-transform: rotateY(0deg); + -ms-transform: rotateY(0deg); + transform: rotateY(0deg); +} + +.form-body.without-side .form-content .form-sent .tick-holder .tick-icon { + -webkit-animation: c-tick-anime3 0.7s cubic-bezier(0.34, 1.61, 0.7, 1) 0s forwards; + -moz-animation: c-tick-anime3 0.7s cubic-bezier(0.34, 1.61, 0.7, 1) 0s forwards; + -ms-animation: c-tick-anime3 0.7s cubic-bezier(0.34, 1.61, 0.7, 1) 0s forwards; + animation: c-tick-anime3 0.7s cubic-bezier(0.34, 1.61, 0.7, 1) 0s forwards; + background-color: rgba(233, 253, 214, 0); +} + +.form-body.without-side .form-content .form-sent .tick-holder .tick-icon:before { + background-color: #8CCB57; +} + +.form-body.without-side .form-content .form-sent .tick-holder .tick-icon:after { + background-color: #8CCB57; +} + +.form-body.without-side .form-content .form-sent h3 { + color: #000; +} + +.form-body.without-side .form-content .form-sent p { + color: #000; +} + +.form-body.without-side .form-content .form-sent .info-holder { + border-top: 1px solid rgba(0, 0, 0, 0.5); +} + +.form-body.without-side .form-content .form-sent .info-holder span { + color: #000; +} + +.form-body.without-side .form-content .form-sent .info-holder a { + color: #000; +} + +.form-body.without-side .form-content .hide-it { + -webkit-transform: rotateY(180deg); + -moz-transform: rotateY(180deg); + -ms-transform: rotateY(180deg); + transform: rotateY(180deg); +} + +@keyframes zoom-in-img { + 0% { + -webkit-transform: scale(1); + -moz-transform: scale(1); + -ms-transform: scale(1); + transform: scale(1); + } + + 50% { + -webkit-transform: scale(1.15); + -moz-transform: scale(1.15); + -ms-transform: scale(1.15); + transform: scale(1.15); + } + + 100% { + -webkit-transform: scale(1); + -moz-transform: scale(1); + -ms-transform: scale(1); + transform: scale(1); + } +} + +/* ----------------------------------- + 2 - Responsive Styles +------------------------------------*/ +@media (max-width: 992px) { + .img-holder { + display: none; + } + + .form-holder { + margin-left: 0; + } + + .form-holder .form-content { + padding: 125px 60px 60px; + } + + .form-body.on-top .form-holder .form-content, .form-body.on-top-mobile .form-holder .form-content { + padding: 60px; + } + + .website-logo { + position: relative; + top: 50px; + left: 50px; + right: initial; + bottom: initial; + } + + .website-logo .logo { + background-image: url("../images/logo-light.svg"); + } + + .form-body.without-side .img-holder { + display: inline-block; + } + + .form-body.without-side .website-logo .logo { + background-image: url("../images/logo-light.svg"); + } + + .form-body.without-side .form-holder .form-content { + padding: 125px 30px 60px; + } + + .form-body.on-top-mobile .website-logo { + position: absolute; + } + + .form-body.on-top-mobile .img-holder { + display: block; + position: relative; + width: 100%; + min-height: initial; + height: initial; + overflow: initial; + padding: 40px; + } + + .form-body.on-top-mobile .img-holder .info-holder.simple-info h3 { + margin-bottom: 16px; + } + + .form-body.on-top-mobile .img-holder .info-holder.simple-info p { + margin-bottom: 10px; + } + + .form-body.on-top-mobile .img-holder .info-holder.simple-info img { + margin-bottom: 20px; + } + + .form-body.on-top-mobile .form-holder { + margin-left: 0; + } +} + +@media (max-width: 575px) { + .form-body.on-top .img-holder, .form-body.on-top-mobile .img-holder { + padding: 90px 40px 40px; + } + + .form-content .row.top-padding .form-button { + text-align: left !important; + margin-top: 30px; + } +} diff --git a/blade-core-oauth2/src/main/resources/static/css/iofrm-theme.css b/blade-core-oauth2/src/main/resources/static/css/iofrm-theme.css new file mode 100644 index 0000000..d25fbf3 --- /dev/null +++ b/blade-core-oauth2/src/main/resources/static/css/iofrm-theme.css @@ -0,0 +1,571 @@ +/*------------------------------------------------------------------ + * Theme Name: iofrm - form templates + * Theme URI: http://www.brandio.io/envato/iofrm + * Author: Brandio + * Author URI: http://www.brandio.io/ + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + -------------------------------------------------------------------*/ + +body { + background-color: #152733; +} + +.form-body { + background-color: #152733; +} + +.website-logo { + display: none; + top: 50px; + left: 50px; + right: initial; + bottom: initial; +} + +.website-logo img { + width: 100px; +} + +.website-logo .logo { + background-image: url("../images/logo-light.svg"); +} + +.website-logo .logo img { + width: 100px; +} + +.website-logo-inside img { + width: 100px; +} + +.website-logo-inside .logo { + background-image: url("../images/logo-light.svg"); +} + +.website-logo-inside .logo img { + width: 100px; +} + +.img-holder { + width: 0; + background-color: #5CBAFF; +} + +.img-holder .info-holder h3 { + color: #fff; + text-align: left; +} + +.img-holder .info-holder h3 span { + color: #fff; +} + +.img-holder .info-holder h2 { + color: #fff; + text-align: left; +} + +.img-holder .info-holder h2 span { + color: #fff; +} + +.img-holder .info-holder p { + color: #fff; + text-align: left; +} + +.img-holder .bg { + opacity: 0.23; + background-image: none; +} + +.form-holder { + margin-left: 0; +} + +.form-holder .form-content ::-webkit-input-placeholder { + color: #8D8D8D !important; +} + +.form-holder .form-content :-moz-placeholder { + color: #8D8D8D !important; +} + +.form-holder .form-content ::-moz-placeholder { + color: #8D8D8D !important; +} + +.form-holder .form-content :-ms-input-placeholder { + color: #8D8D8D !important; +} + +.form-content { + background-color: #152733; +} + +.form-content .form-group { + color: #fff; +} + +.form-content .form-items { + max-width: 380px; + text-align: center; +} + +.form-content .form-icon { + margin-top: calc(-42px - 35px); +} + +.form-content .form-icon .icon-holder { + background-color: #4A77F7; +} + +.form-content h1 { + color: #fff; + text-align: center; +} + +.form-content h2 { + color: #fff; + text-align: center; +} + +.form-content h3 { + color: #fff; + text-align: center; +} + +.form-content p { + color: #fff; + text-align: center; +} + +.form-content label { + color: #fff; + text-align: center; +} + +.form-content .page-links a { + color: #fff; +} + +.form-content .page-links a:after { + background-color: rgba(255, 255, 255, 0.5); +} + +.form-content .page-links a.active:after { + background-color: #fff; +} + +.form-content .page-links a:hover:after, .form-content .page-links a:focus:after { + background-color: #fff; +} + +.form-content input, .form-content .dropdown-toggle.btn-default { + border: 0; + background-color: #fff; + color: #8D8D8D; +} + +.form-content input:hover, .form-content input:focus, .form-content .dropdown-toggle.btn-default:hover, .form-content .dropdown-toggle.btn-default:focus { + border: 0; + background-color: #ebeff8; + color: #8D8D8D; +} + +.form-content textarea { + border: 0; + background-color: #fff; + color: #8D8D8D; +} + +.form-content textarea:hover, .form-content textarea:focus { + border: 0; + background-color: #ebeff8; + color: #8D8D8D; +} + +.form-content .custom-file-label { + border: 0; + background-color: #fff; + color: #8D8D8D; +} + +.form-content .custom-file-label:after { + color: #0093FF; +} + +.form-content .custom-file:hover .custom-file-label, .form-content .custom-file:focus .custom-file-label { + border: 0; + background-color: #ebeff8; + color: #8D8D8D; +} + +.form-content input[type=checkbox]:not(:checked) + label, .form-content input[type=checkbox]:checked + label, .form-content input[type=radio]:not(:checked) + label, .form-content input[type=radio]:checked + label { + color: #fff; + font-weight: 700; +} + +.form-content input[type=checkbox]:checked + label, .form-content input[type=radio]:checked + label { + color: #fff; +} + +.form-content input[type=checkbox]:checked + label:before, .form-content input[type=radio]:checked + label:before { + background: #fff; + border: 0px solid #fff; +} + +.form-content input[type=checkbox]:not(:checked) + label:before, .form-content input[type=radio]:not(:checked) + label:before { + background: transparent; + border: 2px solid #fff; +} + +.form-content input[type=checkbox]:not(:checked) + label:after, .form-content input[type=checkbox]:checked + label:after { + color: #152733; +} + +.form-content input[type=radio]:not(:checked) + label:after, .form-content input[type=radio]:checked + label:after { + background-color: #152733; +} + +.form-content .custom-options input[type=checkbox]:not(:checked) + label, .form-content .custom-options input[type=checkbox]:checked + label, .form-content .custom-options input[type=radio]:not(:checked) + label, .form-content .custom-options input[type=radio]:checked + label { + color: #606060; + background-color: #F7F7F7; +} + +.form-content .custom-options input[type=checkbox]:checked + label, .form-content .custom-options input[type=radio]:checked + label { + color: #fff; + background-color: #1592E6; + -webkit-box-shadow: 0 3px 8px rgba(0, 0, 0, 0.16); + box-shadow: 0 3px 8px rgba(0, 0, 0, 0.16); +} + +.form-content .form-button .lbtn { + margin-top: 10px; + background-color: #cb3444; + color: #fff; + -webkit-box-shadow: 0 0 0 rgba(0, 0, 0, 0.16); + box-shadow: 0 0 0 rgba(0, 0, 0, 0.16); +} + +.form-content .form-button .lbtn:hover, .form-content .form-button .lbtn:focus { + background-color: #cb3444; + color: #fff; + -webkit-box-shadow: 0 5px 6px rgba(0, 0, 0, 0.16); + box-shadow: 0 5px 6px rgba(0, 0, 0, 0.16); +} + +.form-content .form-button .ibtn { + background-color: #1592E6; + color: #fff; + -webkit-box-shadow: 0 0 0 rgba(0, 0, 0, 0.16); + box-shadow: 0 0 0 rgba(0, 0, 0, 0.16); +} + +.form-content .form-button .ibtn:hover, .form-content .form-button .ibtn:focus { + background-color: #1592E6; + color: #fff; + -webkit-box-shadow: 0 5px 6px rgba(0, 0, 0, 0.16); + box-shadow: 0 5px 6px rgba(0, 0, 0, 0.16); +} + +.form-content .form-button a { + color: #fff; +} + +.form-content .other-links span { + color: #fff; +} + +.form-content .other-links a { + color: #fff; +} + +.form-content .form-sent .tick-holder .tick-icon { + background-color: rgba(14, 30, 41, 0); +} + +.form-content .form-sent .tick-holder .tick-icon:before { + background-color: #8CCB57; +} + +.form-content .form-sent .tick-holder .tick-icon:after { + background-color: #8CCB57; +} + +.form-content .form-sent h3 { + color: #fff; +} + +.form-content .form-sent p { + color: #fff; +} + +.form-content .form-sent .info-holder { + color: #fff; + border-top: 1px solid rgba(255, 255, 255, 0.5); +} + +.form-content .form-sent .info-holder span { + color: #fff; +} + +.form-content .form-sent .info-holder a { + color: #fff; +} + +@keyframes tick-anime3 { + 0% { + background-color: rgba(14, 30, 41, 0); + -webkit-transform: rotate(35deg) scale(2); + -moz-transform: rotate(35deg) scale(2); + -ms-transform: rotate(35deg) scale(2); + transform: rotate(35deg) scale(2); + } + + 100% { + background-color: #0E1E29; + -webkit-transform: rotate(45deg) scale(1); + -moz-transform: rotate(45deg) scale(1); + -ms-transform: rotate(45deg) scale(1); + transform: rotate(45deg) scale(1); + } +} + +.alert { + color: #fff; +} + +.alert.alert-primary { + background-color: rgba(226, 240, 255, 0); + border-color: #3a86d6; +} + +.alert.alert-primary hr { + border-top-color: #3a86d6; +} + +.alert.alert-secondary { + background-color: rgba(240, 240, 240, 0); + border-color: #8e9396; +} + +.alert.alert-secondary hr { + border-top-color: #8e9396; +} + +.alert.alert-success { + background-color: rgba(247, 255, 240, 0); + border-color: #8CCB57; +} + +.alert.alert-success hr { + border-top-color: #8CCB57; +} + +.alert.alert-danger { + background-color: rgba(255, 250, 250, 0); + border-color: #F55050; +} + +.alert.alert-danger hr { + border-top-color: #F55050; +} + +.alert.alert-warning { + background-color: rgba(255, 248, 225, 0); + border-color: #f1cb4b; +} + +.alert.alert-warning hr { + border-top-color: #f1cb4b; +} + +.alert.alert-info { + background-color: rgba(220, 237, 241, 0); + border-color: #42bfdb; +} + +.alert.alert-info hr { + border-top-color: #42bfdb; +} + +.alert.alert-light { + background-color: rgba(254, 254, 254, 0); + border-color: #a7a4a4; +} + +.alert.alert-light hr { + border-top-color: #a7a4a4; +} + +.alert.alert-dark { + background-color: rgba(214, 216, 217, 0); + border-color: #525557; +} + +.alert.alert-dark hr { + border-top-color: #525557; +} + +.alert.with-icon.alert-primary:before { + color: #3a86d6; +} + +.alert.with-icon.alert-secondary:before { + color: #8e9396; +} + +.alert.with-icon.alert-success:before { + color: #8CCB57; +} + +.alert.with-icon.alert-danger:before { + color: #F55050; +} + +.alert.with-icon.alert-warning:before { + color: #f1cb4b; +} + +.alert.with-icon.alert-info:before { + color: #42bfdb; +} + +.alert.with-icon.alert-light:before { + color: #a7a4a4; +} + +.alert.with-icon.alert-dark:before { + color: #525557; +} + +.alert a, .alert a.alert-link { + color: #fff; +} + +.alert .close { + color: #727272; +} + +.alert .close span { + color: #727272; +} + +.form-subtitle { + color: #fff; +} + +.rad-with-details .more-info { + color: #fff; +} + +.form-body.without-side h3 { + color: #000; +} + +.form-body.without-side p { + color: #000; +} + +.form-body.without-side label { + color: #000; +} + +.form-body.without-side .img-holder .info-holder img { + display: inline-block; +} + +.form-body.without-side .form-content .form-items { + padding: 35px 30px; + background-color: #fff; +} + +.form-body.without-side .form-content .form-items .other-links .text { + color: #000; +} + +.form-body.without-side .form-content .form-items .other-links a { + color: #000; + background-color: #F7F7F7; +} + +.form-body.without-side .form-content .page-links a { + color: #000; +} + +.form-body.without-side .form-content .page-links a:after { + background-color: rgba(255, 255, 255, 0.5); +} + +.form-body.without-side .form-content .page-links a.active:after { + background-color: #fff; +} + +.form-body.without-side .form-content .page-links a:hover:after, .form-body.without-side .form-content .page-links a:focus:after { + background-color: #fff; +} + +.form-body.without-side .form-content input, .form-body.without-side .form-content .dropdown-toggle.btn-default { + border: 0; + background-color: #fff; + color: #8D8D8D; +} + +.form-body.without-side .form-content input:hover, .form-body.without-side .form-content input:focus, .form-body.without-side .form-content .dropdown-toggle.btn-default:hover, .form-body.without-side .form-content .dropdown-toggle.btn-default:focus { + border: 0; + background-color: #fff; + color: #8D8D8D; +} + +.form-body.without-side .form-content .form-button .lbtn { + background-color: #cb3444; + color: #fff; + -webkit-box-shadow: 0 0 0 rgba(0, 0, 0, 0.16); + box-shadow: 0 0 0 rgba(0, 0, 0, 0.16); +} + +.form-body.without-side .form-content .form-button .lbtn:hover, .form-body.without-side .form-content .form-button .lbtn:focus { + -webkit-box-shadow: 0 5px 6px rgba(0, 0, 0, 0.16); + box-shadow: 0 5px 6px rgba(0, 0, 0, 0.16); +} + +.form-body.without-side .form-content .form-button .ibtn { + background-color: #1592E6; + color: #fff; + -webkit-box-shadow: 0 0 0 rgba(0, 0, 0, 0.16); + box-shadow: 0 0 0 rgba(0, 0, 0, 0.16); +} + +.form-body.without-side .form-content .form-button .ibtn:hover, .form-body.without-side .form-content .form-button .ibtn:focus { + -webkit-box-shadow: 0 5px 6px rgba(0, 0, 0, 0.16); + box-shadow: 0 5px 6px rgba(0, 0, 0, 0.16); +} + +.form-body.without-side .form-content .form-button a { + color: #fff; +} + +/* ----------------------------------- + 2 - Responsive Styles +------------------------------------*/ +@media (max-width: 992px) { + .form-holder { + margin-left: 0; + } + + .website-logo { + top: 50px; + left: 50px; + right: initial; + bottom: initial; + } + + .website-logo .logo { + background-image: url("../images/logo-light.svg"); + } + + .form-body.without-side .website-logo .logo { + background-image: url("../images/logo-light.svg"); + } +} diff --git a/blade-core-oauth2/src/main/resources/static/images/bladex-logo.png b/blade-core-oauth2/src/main/resources/static/images/bladex-logo.png new file mode 100644 index 0000000..98a2478 Binary files /dev/null and b/blade-core-oauth2/src/main/resources/static/images/bladex-logo.png differ diff --git a/blade-core-oauth2/src/main/resources/static/js/bootstrap.min.js b/blade-core-oauth2/src/main/resources/static/js/bootstrap.min.js new file mode 100644 index 0000000..4e07ef4 --- /dev/null +++ b/blade-core-oauth2/src/main/resources/static/js/bootstrap.min.js @@ -0,0 +1,6 @@ +/*! + * Bootstrap v4.6.1 (https://getbootstrap.com/) + * Copyright 2011-2021 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("jquery"),require("popper.js")):"function"==typeof define&&define.amd?define(["exports","jquery","popper.js"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).bootstrap={},t.jQuery,t.Popper)}(this,(function(t,e,n){"use strict";function i(t){return t&&"object"==typeof t&&"default"in t?t:{default:t}}var o=i(e),a=i(n);function s(t,e){for(var n=0;n=4)throw new Error("Bootstrap's JavaScript requires at least jQuery v1.9.1 but less than v4.0.0")}};d.jQueryDetection(),o.default.fn.emulateTransitionEnd=function(t){var e=this,n=!1;return o.default(this).one(d.TRANSITION_END,(function(){n=!0})),setTimeout((function(){n||d.triggerTransitionEnd(e)}),t),this},o.default.event.special[d.TRANSITION_END]={bindType:f,delegateType:f,handle:function(t){if(o.default(t.target).is(this))return t.handleObj.handler.apply(this,arguments)}};var c="bs.alert",h=o.default.fn.alert,g=function(){function t(t){this._element=t}var e=t.prototype;return e.close=function(t){var e=this._element;t&&(e=this._getRootElement(t)),this._triggerCloseEvent(e).isDefaultPrevented()||this._removeElement(e)},e.dispose=function(){o.default.removeData(this._element,c),this._element=null},e._getRootElement=function(t){var e=d.getSelectorFromElement(t),n=!1;return e&&(n=document.querySelector(e)),n||(n=o.default(t).closest(".alert")[0]),n},e._triggerCloseEvent=function(t){var e=o.default.Event("close.bs.alert");return o.default(t).trigger(e),e},e._removeElement=function(t){var e=this;if(o.default(t).removeClass("show"),o.default(t).hasClass("fade")){var n=d.getTransitionDurationFromElement(t);o.default(t).one(d.TRANSITION_END,(function(n){return e._destroyElement(t,n)})).emulateTransitionEnd(n)}else this._destroyElement(t)},e._destroyElement=function(t){o.default(t).detach().trigger("closed.bs.alert").remove()},t._jQueryInterface=function(e){return this.each((function(){var n=o.default(this),i=n.data(c);i||(i=new t(this),n.data(c,i)),"close"===e&&i[e](this)}))},t._handleDismiss=function(t){return function(e){e&&e.preventDefault(),t.close(this)}},l(t,null,[{key:"VERSION",get:function(){return"4.6.1"}}]),t}();o.default(document).on("click.bs.alert.data-api",'[data-dismiss="alert"]',g._handleDismiss(new g)),o.default.fn.alert=g._jQueryInterface,o.default.fn.alert.Constructor=g,o.default.fn.alert.noConflict=function(){return o.default.fn.alert=h,g._jQueryInterface};var m="bs.button",p=o.default.fn.button,_="active",v='[data-toggle^="button"]',y='input:not([type="hidden"])',b=".btn",E=function(){function t(t){this._element=t,this.shouldAvoidTriggerChange=!1}var e=t.prototype;return e.toggle=function(){var t=!0,e=!0,n=o.default(this._element).closest('[data-toggle="buttons"]')[0];if(n){var i=this._element.querySelector(y);if(i){if("radio"===i.type)if(i.checked&&this._element.classList.contains(_))t=!1;else{var a=n.querySelector(".active");a&&o.default(a).removeClass(_)}t&&("checkbox"!==i.type&&"radio"!==i.type||(i.checked=!this._element.classList.contains(_)),this.shouldAvoidTriggerChange||o.default(i).trigger("change")),i.focus(),e=!1}}this._element.hasAttribute("disabled")||this._element.classList.contains("disabled")||(e&&this._element.setAttribute("aria-pressed",!this._element.classList.contains(_)),t&&o.default(this._element).toggleClass(_))},e.dispose=function(){o.default.removeData(this._element,m),this._element=null},t._jQueryInterface=function(e,n){return this.each((function(){var i=o.default(this),a=i.data(m);a||(a=new t(this),i.data(m,a)),a.shouldAvoidTriggerChange=n,"toggle"===e&&a[e]()}))},l(t,null,[{key:"VERSION",get:function(){return"4.6.1"}}]),t}();o.default(document).on("click.bs.button.data-api",v,(function(t){var e=t.target,n=e;if(o.default(e).hasClass("btn")||(e=o.default(e).closest(b)[0]),!e||e.hasAttribute("disabled")||e.classList.contains("disabled"))t.preventDefault();else{var i=e.querySelector(y);if(i&&(i.hasAttribute("disabled")||i.classList.contains("disabled")))return void t.preventDefault();"INPUT"!==n.tagName&&"LABEL"===e.tagName||E._jQueryInterface.call(o.default(e),"toggle","INPUT"===n.tagName)}})).on("focus.bs.button.data-api blur.bs.button.data-api",v,(function(t){var e=o.default(t.target).closest(b)[0];o.default(e).toggleClass("focus",/^focus(in)?$/.test(t.type))})),o.default(window).on("load.bs.button.data-api",(function(){for(var t=[].slice.call(document.querySelectorAll('[data-toggle="buttons"] .btn')),e=0,n=t.length;e0,this._pointerEvent=Boolean(window.PointerEvent||window.MSPointerEvent),this._addEventListeners()}var e=t.prototype;return e.next=function(){this._isSliding||this._slide(N)},e.nextWhenVisible=function(){var t=o.default(this._element);!document.hidden&&t.is(":visible")&&"hidden"!==t.css("visibility")&&this.next()},e.prev=function(){this._isSliding||this._slide(D)},e.pause=function(t){t||(this._isPaused=!0),this._element.querySelector(".carousel-item-next, .carousel-item-prev")&&(d.triggerTransitionEnd(this._element),this.cycle(!0)),clearInterval(this._interval),this._interval=null},e.cycle=function(t){t||(this._isPaused=!1),this._interval&&(clearInterval(this._interval),this._interval=null),this._config.interval&&!this._isPaused&&(this._updateInterval(),this._interval=setInterval((document.visibilityState?this.nextWhenVisible:this.next).bind(this),this._config.interval))},e.to=function(t){var e=this;this._activeElement=this._element.querySelector(I);var n=this._getItemIndex(this._activeElement);if(!(t>this._items.length-1||t<0))if(this._isSliding)o.default(this._element).one(A,(function(){return e.to(t)}));else{if(n===t)return this.pause(),void this.cycle();var i=t>n?N:D;this._slide(i,this._items[t])}},e.dispose=function(){o.default(this._element).off(".bs.carousel"),o.default.removeData(this._element,w),this._items=null,this._config=null,this._element=null,this._interval=null,this._isPaused=null,this._isSliding=null,this._activeElement=null,this._indicatorsElement=null},e._getConfig=function(t){return t=r({},k,t),d.typeCheckConfig(T,t,O),t},e._handleSwipe=function(){var t=Math.abs(this.touchDeltaX);if(!(t<=40)){var e=t/this.touchDeltaX;this.touchDeltaX=0,e>0&&this.prev(),e<0&&this.next()}},e._addEventListeners=function(){var t=this;this._config.keyboard&&o.default(this._element).on("keydown.bs.carousel",(function(e){return t._keydown(e)})),"hover"===this._config.pause&&o.default(this._element).on("mouseenter.bs.carousel",(function(e){return t.pause(e)})).on("mouseleave.bs.carousel",(function(e){return t.cycle(e)})),this._config.touch&&this._addTouchEventListeners()},e._addTouchEventListeners=function(){var t=this;if(this._touchSupported){var e=function(e){t._pointerEvent&&j[e.originalEvent.pointerType.toUpperCase()]?t.touchStartX=e.originalEvent.clientX:t._pointerEvent||(t.touchStartX=e.originalEvent.touches[0].clientX)},n=function(e){t._pointerEvent&&j[e.originalEvent.pointerType.toUpperCase()]&&(t.touchDeltaX=e.originalEvent.clientX-t.touchStartX),t._handleSwipe(),"hover"===t._config.pause&&(t.pause(),t.touchTimeout&&clearTimeout(t.touchTimeout),t.touchTimeout=setTimeout((function(e){return t.cycle(e)}),500+t._config.interval))};o.default(this._element.querySelectorAll(".carousel-item img")).on("dragstart.bs.carousel",(function(t){return t.preventDefault()})),this._pointerEvent?(o.default(this._element).on("pointerdown.bs.carousel",(function(t){return e(t)})),o.default(this._element).on("pointerup.bs.carousel",(function(t){return n(t)})),this._element.classList.add("pointer-event")):(o.default(this._element).on("touchstart.bs.carousel",(function(t){return e(t)})),o.default(this._element).on("touchmove.bs.carousel",(function(e){return function(e){t.touchDeltaX=e.originalEvent.touches&&e.originalEvent.touches.length>1?0:e.originalEvent.touches[0].clientX-t.touchStartX}(e)})),o.default(this._element).on("touchend.bs.carousel",(function(t){return n(t)})))}},e._keydown=function(t){if(!/input|textarea/i.test(t.target.tagName))switch(t.which){case 37:t.preventDefault(),this.prev();break;case 39:t.preventDefault(),this.next()}},e._getItemIndex=function(t){return this._items=t&&t.parentNode?[].slice.call(t.parentNode.querySelectorAll(".carousel-item")):[],this._items.indexOf(t)},e._getItemByDirection=function(t,e){var n=t===N,i=t===D,o=this._getItemIndex(e),a=this._items.length-1;if((i&&0===o||n&&o===a)&&!this._config.wrap)return e;var s=(o+(t===D?-1:1))%this._items.length;return-1===s?this._items[this._items.length-1]:this._items[s]},e._triggerSlideEvent=function(t,e){var n=this._getItemIndex(t),i=this._getItemIndex(this._element.querySelector(I)),a=o.default.Event("slide.bs.carousel",{relatedTarget:t,direction:e,from:i,to:n});return o.default(this._element).trigger(a),a},e._setActiveIndicatorElement=function(t){if(this._indicatorsElement){var e=[].slice.call(this._indicatorsElement.querySelectorAll(".active"));o.default(e).removeClass(S);var n=this._indicatorsElement.children[this._getItemIndex(t)];n&&o.default(n).addClass(S)}},e._updateInterval=function(){var t=this._activeElement||this._element.querySelector(I);if(t){var e=parseInt(t.getAttribute("data-interval"),10);e?(this._config.defaultInterval=this._config.defaultInterval||this._config.interval,this._config.interval=e):this._config.interval=this._config.defaultInterval||this._config.interval}},e._slide=function(t,e){var n,i,a,s=this,l=this._element.querySelector(I),r=this._getItemIndex(l),u=e||l&&this._getItemByDirection(t,l),f=this._getItemIndex(u),c=Boolean(this._interval);if(t===N?(n="carousel-item-left",i="carousel-item-next",a="left"):(n="carousel-item-right",i="carousel-item-prev",a="right"),u&&o.default(u).hasClass(S))this._isSliding=!1;else if(!this._triggerSlideEvent(u,a).isDefaultPrevented()&&l&&u){this._isSliding=!0,c&&this.pause(),this._setActiveIndicatorElement(u),this._activeElement=u;var h=o.default.Event(A,{relatedTarget:u,direction:a,from:r,to:f});if(o.default(this._element).hasClass("slide")){o.default(u).addClass(i),d.reflow(u),o.default(l).addClass(n),o.default(u).addClass(n);var g=d.getTransitionDurationFromElement(l);o.default(l).one(d.TRANSITION_END,(function(){o.default(u).removeClass(n+" "+i).addClass(S),o.default(l).removeClass("active "+i+" "+n),s._isSliding=!1,setTimeout((function(){return o.default(s._element).trigger(h)}),0)})).emulateTransitionEnd(g)}else o.default(l).removeClass(S),o.default(u).addClass(S),this._isSliding=!1,o.default(this._element).trigger(h);c&&this.cycle()}},t._jQueryInterface=function(e){return this.each((function(){var n=o.default(this).data(w),i=r({},k,o.default(this).data());"object"==typeof e&&(i=r({},i,e));var a="string"==typeof e?e:i.slide;if(n||(n=new t(this,i),o.default(this).data(w,n)),"number"==typeof e)n.to(e);else if("string"==typeof a){if("undefined"==typeof n[a])throw new TypeError('No method named "'+a+'"');n[a]()}else i.interval&&i.ride&&(n.pause(),n.cycle())}))},t._dataApiClickHandler=function(e){var n=d.getSelectorFromElement(this);if(n){var i=o.default(n)[0];if(i&&o.default(i).hasClass("carousel")){var a=r({},o.default(i).data(),o.default(this).data()),s=this.getAttribute("data-slide-to");s&&(a.interval=!1),t._jQueryInterface.call(o.default(i),a),s&&o.default(i).data(w).to(s),e.preventDefault()}}},l(t,null,[{key:"VERSION",get:function(){return"4.6.1"}},{key:"Default",get:function(){return k}}]),t}();o.default(document).on("click.bs.carousel.data-api","[data-slide], [data-slide-to]",P._dataApiClickHandler),o.default(window).on("load.bs.carousel.data-api",(function(){for(var t=[].slice.call(document.querySelectorAll('[data-ride="carousel"]')),e=0,n=t.length;e0&&(this._selector=s,this._triggerArray.push(a))}this._parent=this._config.parent?this._getParent():null,this._config.parent||this._addAriaAndCollapsedClass(this._element,this._triggerArray),this._config.toggle&&this.toggle()}var e=t.prototype;return e.toggle=function(){o.default(this._element).hasClass(q)?this.hide():this.show()},e.show=function(){var e,n,i=this;if(!(this._isTransitioning||o.default(this._element).hasClass(q)||(this._parent&&0===(e=[].slice.call(this._parent.querySelectorAll(".show, .collapsing")).filter((function(t){return"string"==typeof i._config.parent?t.getAttribute("data-parent")===i._config.parent:t.classList.contains(F)}))).length&&(e=null),e&&(n=o.default(e).not(this._selector).data(R))&&n._isTransitioning))){var a=o.default.Event("show.bs.collapse");if(o.default(this._element).trigger(a),!a.isDefaultPrevented()){e&&(t._jQueryInterface.call(o.default(e).not(this._selector),"hide"),n||o.default(e).data(R,null));var s=this._getDimension();o.default(this._element).removeClass(F).addClass(Q),this._element.style[s]=0,this._triggerArray.length&&o.default(this._triggerArray).removeClass(B).attr("aria-expanded",!0),this.setTransitioning(!0);var l="scroll"+(s[0].toUpperCase()+s.slice(1)),r=d.getTransitionDurationFromElement(this._element);o.default(this._element).one(d.TRANSITION_END,(function(){o.default(i._element).removeClass(Q).addClass("collapse show"),i._element.style[s]="",i.setTransitioning(!1),o.default(i._element).trigger("shown.bs.collapse")})).emulateTransitionEnd(r),this._element.style[s]=this._element[l]+"px"}}},e.hide=function(){var t=this;if(!this._isTransitioning&&o.default(this._element).hasClass(q)){var e=o.default.Event("hide.bs.collapse");if(o.default(this._element).trigger(e),!e.isDefaultPrevented()){var n=this._getDimension();this._element.style[n]=this._element.getBoundingClientRect()[n]+"px",d.reflow(this._element),o.default(this._element).addClass(Q).removeClass("collapse show");var i=this._triggerArray.length;if(i>0)for(var a=0;a0},e._getOffset=function(){var t=this,e={};return"function"==typeof this._config.offset?e.fn=function(e){return e.offsets=r({},e.offsets,t._config.offset(e.offsets,t._element)),e}:e.offset=this._config.offset,e},e._getPopperConfig=function(){var t={placement:this._getPlacement(),modifiers:{offset:this._getOffset(),flip:{enabled:this._config.flip},preventOverflow:{boundariesElement:this._config.boundary}}};return"static"===this._config.display&&(t.modifiers.applyStyle={enabled:!1}),r({},t,this._config.popperConfig)},t._jQueryInterface=function(e){return this.each((function(){var n=o.default(this).data(K);if(n||(n=new t(this,"object"==typeof e?e:null),o.default(this).data(K,n)),"string"==typeof e){if("undefined"==typeof n[e])throw new TypeError('No method named "'+e+'"');n[e]()}}))},t._clearMenus=function(e){if(!e||3!==e.which&&("keyup"!==e.type||9===e.which))for(var n=[].slice.call(document.querySelectorAll(it)),i=0,a=n.length;i0&&s--,40===e.which&&sdocument.documentElement.clientHeight;n||(this._element.style.overflowY="hidden"),this._element.classList.add(ht);var i=d.getTransitionDurationFromElement(this._dialog);o.default(this._element).off(d.TRANSITION_END),o.default(this._element).one(d.TRANSITION_END,(function(){t._element.classList.remove(ht),n||o.default(t._element).one(d.TRANSITION_END,(function(){t._element.style.overflowY=""})).emulateTransitionEnd(t._element,i)})).emulateTransitionEnd(i),this._element.focus()}},e._showElement=function(t){var e=this,n=o.default(this._element).hasClass(dt),i=this._dialog?this._dialog.querySelector(".modal-body"):null;this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE||document.body.appendChild(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),o.default(this._dialog).hasClass("modal-dialog-scrollable")&&i?i.scrollTop=0:this._element.scrollTop=0,n&&d.reflow(this._element),o.default(this._element).addClass(ct),this._config.focus&&this._enforceFocus();var a=o.default.Event("shown.bs.modal",{relatedTarget:t}),s=function(){e._config.focus&&e._element.focus(),e._isTransitioning=!1,o.default(e._element).trigger(a)};if(n){var l=d.getTransitionDurationFromElement(this._dialog);o.default(this._dialog).one(d.TRANSITION_END,s).emulateTransitionEnd(l)}else s()},e._enforceFocus=function(){var t=this;o.default(document).off(pt).on(pt,(function(e){document!==e.target&&t._element!==e.target&&0===o.default(t._element).has(e.target).length&&t._element.focus()}))},e._setEscapeEvent=function(){var t=this;this._isShown?o.default(this._element).on(yt,(function(e){t._config.keyboard&&27===e.which?(e.preventDefault(),t.hide()):t._config.keyboard||27!==e.which||t._triggerBackdropTransition()})):this._isShown||o.default(this._element).off(yt)},e._setResizeEvent=function(){var t=this;this._isShown?o.default(window).on(_t,(function(e){return t.handleUpdate(e)})):o.default(window).off(_t)},e._hideModal=function(){var t=this;this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._showBackdrop((function(){o.default(document.body).removeClass(ft),t._resetAdjustments(),t._resetScrollbar(),o.default(t._element).trigger(gt)}))},e._removeBackdrop=function(){this._backdrop&&(o.default(this._backdrop).remove(),this._backdrop=null)},e._showBackdrop=function(t){var e=this,n=o.default(this._element).hasClass(dt)?dt:"";if(this._isShown&&this._config.backdrop){if(this._backdrop=document.createElement("div"),this._backdrop.className="modal-backdrop",n&&this._backdrop.classList.add(n),o.default(this._backdrop).appendTo(document.body),o.default(this._element).on(vt,(function(t){e._ignoreBackdropClick?e._ignoreBackdropClick=!1:t.target===t.currentTarget&&("static"===e._config.backdrop?e._triggerBackdropTransition():e.hide())})),n&&d.reflow(this._backdrop),o.default(this._backdrop).addClass(ct),!t)return;if(!n)return void t();var i=d.getTransitionDurationFromElement(this._backdrop);o.default(this._backdrop).one(d.TRANSITION_END,t).emulateTransitionEnd(i)}else if(!this._isShown&&this._backdrop){o.default(this._backdrop).removeClass(ct);var a=function(){e._removeBackdrop(),t&&t()};if(o.default(this._element).hasClass(dt)){var s=d.getTransitionDurationFromElement(this._backdrop);o.default(this._backdrop).one(d.TRANSITION_END,a).emulateTransitionEnd(s)}else a()}else t&&t()},e._adjustDialog=function(){var t=this._element.scrollHeight>document.documentElement.clientHeight;!this._isBodyOverflowing&&t&&(this._element.style.paddingLeft=this._scrollbarWidth+"px"),this._isBodyOverflowing&&!t&&(this._element.style.paddingRight=this._scrollbarWidth+"px")},e._resetAdjustments=function(){this._element.style.paddingLeft="",this._element.style.paddingRight=""},e._checkScrollbar=function(){var t=document.body.getBoundingClientRect();this._isBodyOverflowing=Math.round(t.left+t.right)

',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:0,container:!1,fallbackPlacement:"flip",boundary:"scrollParent",customClass:"",sanitize:!0,sanitizeFn:null,whiteList:{"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],div:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},popperConfig:null},Ut={animation:"boolean",template:"string",title:"(string|element|function)",trigger:"string",delay:"(number|object)",html:"boolean",selector:"(string|boolean)",placement:"(string|function)",offset:"(number|string|function)",container:"(string|element|boolean)",fallbackPlacement:"(string|array)",boundary:"(string|element)",customClass:"(string|function)",sanitize:"boolean",sanitizeFn:"(null|function)",whiteList:"object",popperConfig:"(null|object)"},Mt={HIDE:"hide.bs.tooltip",HIDDEN:"hidden.bs.tooltip",SHOW:"show.bs.tooltip",SHOWN:"shown.bs.tooltip",INSERTED:"inserted.bs.tooltip",CLICK:"click.bs.tooltip",FOCUSIN:"focusin.bs.tooltip",FOCUSOUT:"focusout.bs.tooltip",MOUSEENTER:"mouseenter.bs.tooltip",MOUSELEAVE:"mouseleave.bs.tooltip"},Wt=function(){function t(t,e){if("undefined"==typeof a.default)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org)");this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this.element=t,this.config=this._getConfig(e),this.tip=null,this._setListeners()}var e=t.prototype;return e.enable=function(){this._isEnabled=!0},e.disable=function(){this._isEnabled=!1},e.toggleEnabled=function(){this._isEnabled=!this._isEnabled},e.toggle=function(t){if(this._isEnabled)if(t){var e=this.constructor.DATA_KEY,n=o.default(t.currentTarget).data(e);n||(n=new this.constructor(t.currentTarget,this._getDelegateConfig()),o.default(t.currentTarget).data(e,n)),n._activeTrigger.click=!n._activeTrigger.click,n._isWithActiveTrigger()?n._enter(null,n):n._leave(null,n)}else{if(o.default(this.getTipElement()).hasClass(Rt))return void this._leave(null,this);this._enter(null,this)}},e.dispose=function(){clearTimeout(this._timeout),o.default.removeData(this.element,this.constructor.DATA_KEY),o.default(this.element).off(this.constructor.EVENT_KEY),o.default(this.element).closest(".modal").off("hide.bs.modal",this._hideModalHandler),this.tip&&o.default(this.tip).remove(),this._isEnabled=null,this._timeout=null,this._hoverState=null,this._activeTrigger=null,this._popper&&this._popper.destroy(),this._popper=null,this.element=null,this.config=null,this.tip=null},e.show=function(){var t=this;if("none"===o.default(this.element).css("display"))throw new Error("Please use show on visible elements");var e=o.default.Event(this.constructor.Event.SHOW);if(this.isWithContent()&&this._isEnabled){o.default(this.element).trigger(e);var n=d.findShadowRoot(this.element),i=o.default.contains(null!==n?n:this.element.ownerDocument.documentElement,this.element);if(e.isDefaultPrevented()||!i)return;var s=this.getTipElement(),l=d.getUID(this.constructor.NAME);s.setAttribute("id",l),this.element.setAttribute("aria-describedby",l),this.setContent(),this.config.animation&&o.default(s).addClass(Lt);var r="function"==typeof this.config.placement?this.config.placement.call(this,s,this.element):this.config.placement,u=this._getAttachment(r);this.addAttachmentClass(u);var f=this._getContainer();o.default(s).data(this.constructor.DATA_KEY,this),o.default.contains(this.element.ownerDocument.documentElement,this.tip)||o.default(s).appendTo(f),o.default(this.element).trigger(this.constructor.Event.INSERTED),this._popper=new a.default(this.element,s,this._getPopperConfig(u)),o.default(s).addClass(Rt),o.default(s).addClass(this.config.customClass),"ontouchstart"in document.documentElement&&o.default(document.body).children().on("mouseover",null,o.default.noop);var c=function(){t.config.animation&&t._fixTransition();var e=t._hoverState;t._hoverState=null,o.default(t.element).trigger(t.constructor.Event.SHOWN),e===qt&&t._leave(null,t)};if(o.default(this.tip).hasClass(Lt)){var h=d.getTransitionDurationFromElement(this.tip);o.default(this.tip).one(d.TRANSITION_END,c).emulateTransitionEnd(h)}else c()}},e.hide=function(t){var e=this,n=this.getTipElement(),i=o.default.Event(this.constructor.Event.HIDE),a=function(){e._hoverState!==xt&&n.parentNode&&n.parentNode.removeChild(n),e._cleanTipClass(),e.element.removeAttribute("aria-describedby"),o.default(e.element).trigger(e.constructor.Event.HIDDEN),null!==e._popper&&e._popper.destroy(),t&&t()};if(o.default(this.element).trigger(i),!i.isDefaultPrevented()){if(o.default(n).removeClass(Rt),"ontouchstart"in document.documentElement&&o.default(document.body).children().off("mouseover",null,o.default.noop),this._activeTrigger.click=!1,this._activeTrigger.focus=!1,this._activeTrigger.hover=!1,o.default(this.tip).hasClass(Lt)){var s=d.getTransitionDurationFromElement(n);o.default(n).one(d.TRANSITION_END,a).emulateTransitionEnd(s)}else a();this._hoverState=""}},e.update=function(){null!==this._popper&&this._popper.scheduleUpdate()},e.isWithContent=function(){return Boolean(this.getTitle())},e.addAttachmentClass=function(t){o.default(this.getTipElement()).addClass("bs-tooltip-"+t)},e.getTipElement=function(){return this.tip=this.tip||o.default(this.config.template)[0],this.tip},e.setContent=function(){var t=this.getTipElement();this.setElementContent(o.default(t.querySelectorAll(".tooltip-inner")),this.getTitle()),o.default(t).removeClass("fade show")},e.setElementContent=function(t,e){"object"!=typeof e||!e.nodeType&&!e.jquery?this.config.html?(this.config.sanitize&&(e=At(e,this.config.whiteList,this.config.sanitizeFn)),t.html(e)):t.text(e):this.config.html?o.default(e).parent().is(t)||t.empty().append(e):t.text(o.default(e).text())},e.getTitle=function(){var t=this.element.getAttribute("data-original-title");return t||(t="function"==typeof this.config.title?this.config.title.call(this.element):this.config.title),t},e._getPopperConfig=function(t){var e=this;return r({},{placement:t,modifiers:{offset:this._getOffset(),flip:{behavior:this.config.fallbackPlacement},arrow:{element:".arrow"},preventOverflow:{boundariesElement:this.config.boundary}},onCreate:function(t){t.originalPlacement!==t.placement&&e._handlePopperPlacementChange(t)},onUpdate:function(t){return e._handlePopperPlacementChange(t)}},this.config.popperConfig)},e._getOffset=function(){var t=this,e={};return"function"==typeof this.config.offset?e.fn=function(e){return e.offsets=r({},e.offsets,t.config.offset(e.offsets,t.element)),e}:e.offset=this.config.offset,e},e._getContainer=function(){return!1===this.config.container?document.body:d.isElement(this.config.container)?o.default(this.config.container):o.default(document).find(this.config.container)},e._getAttachment=function(t){return Bt[t.toUpperCase()]},e._setListeners=function(){var t=this;this.config.trigger.split(" ").forEach((function(e){if("click"===e)o.default(t.element).on(t.constructor.Event.CLICK,t.config.selector,(function(e){return t.toggle(e)}));else if("manual"!==e){var n=e===Ft?t.constructor.Event.MOUSEENTER:t.constructor.Event.FOCUSIN,i=e===Ft?t.constructor.Event.MOUSELEAVE:t.constructor.Event.FOCUSOUT;o.default(t.element).on(n,t.config.selector,(function(e){return t._enter(e)})).on(i,t.config.selector,(function(e){return t._leave(e)}))}})),this._hideModalHandler=function(){t.element&&t.hide()},o.default(this.element).closest(".modal").on("hide.bs.modal",this._hideModalHandler),this.config.selector?this.config=r({},this.config,{trigger:"manual",selector:""}):this._fixTitle()},e._fixTitle=function(){var t=typeof this.element.getAttribute("data-original-title");(this.element.getAttribute("title")||"string"!==t)&&(this.element.setAttribute("data-original-title",this.element.getAttribute("title")||""),this.element.setAttribute("title",""))},e._enter=function(t,e){var n=this.constructor.DATA_KEY;(e=e||o.default(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),o.default(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusin"===t.type?Qt:Ft]=!0),o.default(e.getTipElement()).hasClass(Rt)||e._hoverState===xt?e._hoverState=xt:(clearTimeout(e._timeout),e._hoverState=xt,e.config.delay&&e.config.delay.show?e._timeout=setTimeout((function(){e._hoverState===xt&&e.show()}),e.config.delay.show):e.show())},e._leave=function(t,e){var n=this.constructor.DATA_KEY;(e=e||o.default(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),o.default(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusout"===t.type?Qt:Ft]=!1),e._isWithActiveTrigger()||(clearTimeout(e._timeout),e._hoverState=qt,e.config.delay&&e.config.delay.hide?e._timeout=setTimeout((function(){e._hoverState===qt&&e.hide()}),e.config.delay.hide):e.hide())},e._isWithActiveTrigger=function(){for(var t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1},e._getConfig=function(t){var e=o.default(this.element).data();return Object.keys(e).forEach((function(t){-1!==Pt.indexOf(t)&&delete e[t]})),"number"==typeof(t=r({},this.constructor.Default,e,"object"==typeof t&&t?t:{})).delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),d.typeCheckConfig(It,t,this.constructor.DefaultType),t.sanitize&&(t.template=At(t.template,t.whiteList,t.sanitizeFn)),t},e._getDelegateConfig=function(){var t={};if(this.config)for(var e in this.config)this.constructor.Default[e]!==this.config[e]&&(t[e]=this.config[e]);return t},e._cleanTipClass=function(){var t=o.default(this.getTipElement()),e=t.attr("class").match(jt);null!==e&&e.length&&t.removeClass(e.join(""))},e._handlePopperPlacementChange=function(t){this.tip=t.instance.popper,this._cleanTipClass(),this.addAttachmentClass(this._getAttachment(t.placement))},e._fixTransition=function(){var t=this.getTipElement(),e=this.config.animation;null===t.getAttribute("x-placement")&&(o.default(t).removeClass(Lt),this.config.animation=!1,this.hide(),this.show(),this.config.animation=e)},t._jQueryInterface=function(e){return this.each((function(){var n=o.default(this),i=n.data(kt),a="object"==typeof e&&e;if((i||!/dispose|hide/.test(e))&&(i||(i=new t(this,a),n.data(kt,i)),"string"==typeof e)){if("undefined"==typeof i[e])throw new TypeError('No method named "'+e+'"');i[e]()}}))},l(t,null,[{key:"VERSION",get:function(){return"4.6.1"}},{key:"Default",get:function(){return Ht}},{key:"NAME",get:function(){return It}},{key:"DATA_KEY",get:function(){return kt}},{key:"Event",get:function(){return Mt}},{key:"EVENT_KEY",get:function(){return".bs.tooltip"}},{key:"DefaultType",get:function(){return Ut}}]),t}();o.default.fn.tooltip=Wt._jQueryInterface,o.default.fn.tooltip.Constructor=Wt,o.default.fn.tooltip.noConflict=function(){return o.default.fn.tooltip=Ot,Wt._jQueryInterface};var Vt="bs.popover",zt=o.default.fn.popover,Kt=new RegExp("(^|\\s)bs-popover\\S+","g"),Xt=r({},Wt.Default,{placement:"right",trigger:"click",content:"",template:''}),Yt=r({},Wt.DefaultType,{content:"(string|element|function)"}),$t={HIDE:"hide.bs.popover",HIDDEN:"hidden.bs.popover",SHOW:"show.bs.popover",SHOWN:"shown.bs.popover",INSERTED:"inserted.bs.popover",CLICK:"click.bs.popover",FOCUSIN:"focusin.bs.popover",FOCUSOUT:"focusout.bs.popover",MOUSEENTER:"mouseenter.bs.popover",MOUSELEAVE:"mouseleave.bs.popover"},Jt=function(t){var e,n;function i(){return t.apply(this,arguments)||this}n=t,(e=i).prototype=Object.create(n.prototype),e.prototype.constructor=e,u(e,n);var a=i.prototype;return a.isWithContent=function(){return this.getTitle()||this._getContent()},a.addAttachmentClass=function(t){o.default(this.getTipElement()).addClass("bs-popover-"+t)},a.getTipElement=function(){return this.tip=this.tip||o.default(this.config.template)[0],this.tip},a.setContent=function(){var t=o.default(this.getTipElement());this.setElementContent(t.find(".popover-header"),this.getTitle());var e=this._getContent();"function"==typeof e&&(e=e.call(this.element)),this.setElementContent(t.find(".popover-body"),e),t.removeClass("fade show")},a._getContent=function(){return this.element.getAttribute("data-content")||this.config.content},a._cleanTipClass=function(){var t=o.default(this.getTipElement()),e=t.attr("class").match(Kt);null!==e&&e.length>0&&t.removeClass(e.join(""))},i._jQueryInterface=function(t){return this.each((function(){var e=o.default(this).data(Vt),n="object"==typeof t?t:null;if((e||!/dispose|hide/.test(t))&&(e||(e=new i(this,n),o.default(this).data(Vt,e)),"string"==typeof t)){if("undefined"==typeof e[t])throw new TypeError('No method named "'+t+'"');e[t]()}}))},l(i,null,[{key:"VERSION",get:function(){return"4.6.1"}},{key:"Default",get:function(){return Xt}},{key:"NAME",get:function(){return"popover"}},{key:"DATA_KEY",get:function(){return Vt}},{key:"Event",get:function(){return $t}},{key:"EVENT_KEY",get:function(){return".bs.popover"}},{key:"DefaultType",get:function(){return Yt}}]),i}(Wt);o.default.fn.popover=Jt._jQueryInterface,o.default.fn.popover.Constructor=Jt,o.default.fn.popover.noConflict=function(){return o.default.fn.popover=zt,Jt._jQueryInterface};var Gt="scrollspy",Zt="bs.scrollspy",te=o.default.fn[Gt],ee="active",ne="position",ie=".nav, .list-group",oe={offset:10,method:"auto",target:""},ae={offset:"number",method:"string",target:"(string|element)"},se=function(){function t(t,e){var n=this;this._element=t,this._scrollElement="BODY"===t.tagName?window:t,this._config=this._getConfig(e),this._selector=this._config.target+" .nav-link,"+this._config.target+" .list-group-item,"+this._config.target+" .dropdown-item",this._offsets=[],this._targets=[],this._activeTarget=null,this._scrollHeight=0,o.default(this._scrollElement).on("scroll.bs.scrollspy",(function(t){return n._process(t)})),this.refresh(),this._process()}var e=t.prototype;return e.refresh=function(){var t=this,e=this._scrollElement===this._scrollElement.window?"offset":ne,n="auto"===this._config.method?e:this._config.method,i=n===ne?this._getScrollTop():0;this._offsets=[],this._targets=[],this._scrollHeight=this._getScrollHeight(),[].slice.call(document.querySelectorAll(this._selector)).map((function(t){var e,a=d.getSelectorFromElement(t);if(a&&(e=document.querySelector(a)),e){var s=e.getBoundingClientRect();if(s.width||s.height)return[o.default(e)[n]().top+i,a]}return null})).filter((function(t){return t})).sort((function(t,e){return t[0]-e[0]})).forEach((function(e){t._offsets.push(e[0]),t._targets.push(e[1])}))},e.dispose=function(){o.default.removeData(this._element,Zt),o.default(this._scrollElement).off(".bs.scrollspy"),this._element=null,this._scrollElement=null,this._config=null,this._selector=null,this._offsets=null,this._targets=null,this._activeTarget=null,this._scrollHeight=null},e._getConfig=function(t){if("string"!=typeof(t=r({},oe,"object"==typeof t&&t?t:{})).target&&d.isElement(t.target)){var e=o.default(t.target).attr("id");e||(e=d.getUID(Gt),o.default(t.target).attr("id",e)),t.target="#"+e}return d.typeCheckConfig(Gt,t,ae),t},e._getScrollTop=function(){return this._scrollElement===window?this._scrollElement.pageYOffset:this._scrollElement.scrollTop},e._getScrollHeight=function(){return this._scrollElement.scrollHeight||Math.max(document.body.scrollHeight,document.documentElement.scrollHeight)},e._getOffsetHeight=function(){return this._scrollElement===window?window.innerHeight:this._scrollElement.getBoundingClientRect().height},e._process=function(){var t=this._getScrollTop()+this._config.offset,e=this._getScrollHeight(),n=this._config.offset+e-this._getOffsetHeight();if(this._scrollHeight!==e&&this.refresh(),t>=n){var i=this._targets[this._targets.length-1];this._activeTarget!==i&&this._activate(i)}else{if(this._activeTarget&&t0)return this._activeTarget=null,void this._clear();for(var o=this._offsets.length;o--;)this._activeTarget!==this._targets[o]&&t>=this._offsets[o]&&("undefined"==typeof this._offsets[o+1]||t li > .active",ge=function(){function t(t){this._element=t}var e=t.prototype;return e.show=function(){var t=this;if(!(this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE&&o.default(this._element).hasClass(ue)||o.default(this._element).hasClass("disabled"))){var e,n,i=o.default(this._element).closest(".nav, .list-group")[0],a=d.getSelectorFromElement(this._element);if(i){var s="UL"===i.nodeName||"OL"===i.nodeName?he:ce;n=(n=o.default.makeArray(o.default(i).find(s)))[n.length-1]}var l=o.default.Event("hide.bs.tab",{relatedTarget:this._element}),r=o.default.Event("show.bs.tab",{relatedTarget:n});if(n&&o.default(n).trigger(l),o.default(this._element).trigger(r),!r.isDefaultPrevented()&&!l.isDefaultPrevented()){a&&(e=document.querySelector(a)),this._activate(this._element,i);var u=function(){var e=o.default.Event("hidden.bs.tab",{relatedTarget:t._element}),i=o.default.Event("shown.bs.tab",{relatedTarget:n});o.default(n).trigger(e),o.default(t._element).trigger(i)};e?this._activate(e,e.parentNode,u):u()}}},e.dispose=function(){o.default.removeData(this._element,le),this._element=null},e._activate=function(t,e,n){var i=this,a=(!e||"UL"!==e.nodeName&&"OL"!==e.nodeName?o.default(e).children(ce):o.default(e).find(he))[0],s=n&&a&&o.default(a).hasClass(fe),l=function(){return i._transitionComplete(t,a,n)};if(a&&s){var r=d.getTransitionDurationFromElement(a);o.default(a).removeClass(de).one(d.TRANSITION_END,l).emulateTransitionEnd(r)}else l()},e._transitionComplete=function(t,e,n){if(e){o.default(e).removeClass(ue);var i=o.default(e.parentNode).find("> .dropdown-menu .active")[0];i&&o.default(i).removeClass(ue),"tab"===e.getAttribute("role")&&e.setAttribute("aria-selected",!1)}o.default(t).addClass(ue),"tab"===t.getAttribute("role")&&t.setAttribute("aria-selected",!0),d.reflow(t),t.classList.contains(fe)&&t.classList.add(de);var a=t.parentNode;if(a&&"LI"===a.nodeName&&(a=a.parentNode),a&&o.default(a).hasClass("dropdown-menu")){var s=o.default(t).closest(".dropdown")[0];if(s){var l=[].slice.call(s.querySelectorAll(".dropdown-toggle"));o.default(l).addClass(ue)}t.setAttribute("aria-expanded",!0)}n&&n()},t._jQueryInterface=function(e){return this.each((function(){var n=o.default(this),i=n.data(le);if(i||(i=new t(this),n.data(le,i)),"string"==typeof e){if("undefined"==typeof i[e])throw new TypeError('No method named "'+e+'"');i[e]()}}))},l(t,null,[{key:"VERSION",get:function(){return"4.6.1"}}]),t}();o.default(document).on("click.bs.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"], [data-toggle="list"]',(function(t){t.preventDefault(),ge._jQueryInterface.call(o.default(this),"show")})),o.default.fn.tab=ge._jQueryInterface,o.default.fn.tab.Constructor=ge,o.default.fn.tab.noConflict=function(){return o.default.fn.tab=re,ge._jQueryInterface};var me="bs.toast",pe=o.default.fn.toast,_e="hide",ve="show",ye="showing",be="click.dismiss.bs.toast",Ee={animation:!0,autohide:!0,delay:500},Te={animation:"boolean",autohide:"boolean",delay:"number"},we=function(){function t(t,e){this._element=t,this._config=this._getConfig(e),this._timeout=null,this._setListeners()}var e=t.prototype;return e.show=function(){var t=this,e=o.default.Event("show.bs.toast");if(o.default(this._element).trigger(e),!e.isDefaultPrevented()){this._clearTimeout(),this._config.animation&&this._element.classList.add("fade");var n=function(){t._element.classList.remove(ye),t._element.classList.add(ve),o.default(t._element).trigger("shown.bs.toast"),t._config.autohide&&(t._timeout=setTimeout((function(){t.hide()}),t._config.delay))};if(this._element.classList.remove(_e),d.reflow(this._element),this._element.classList.add(ye),this._config.animation){var i=d.getTransitionDurationFromElement(this._element);o.default(this._element).one(d.TRANSITION_END,n).emulateTransitionEnd(i)}else n()}},e.hide=function(){if(this._element.classList.contains(ve)){var t=o.default.Event("hide.bs.toast");o.default(this._element).trigger(t),t.isDefaultPrevented()||this._close()}},e.dispose=function(){this._clearTimeout(),this._element.classList.contains(ve)&&this._element.classList.remove(ve),o.default(this._element).off(be),o.default.removeData(this._element,me),this._element=null,this._config=null},e._getConfig=function(t){return t=r({},Ee,o.default(this._element).data(),"object"==typeof t&&t?t:{}),d.typeCheckConfig("toast",t,this.constructor.DefaultType),t},e._setListeners=function(){var t=this;o.default(this._element).on(be,'[data-dismiss="toast"]',(function(){return t.hide()}))},e._close=function(){var t=this,e=function(){t._element.classList.add(_e),o.default(t._element).trigger("hidden.bs.toast")};if(this._element.classList.remove(ve),this._config.animation){var n=d.getTransitionDurationFromElement(this._element);o.default(this._element).one(d.TRANSITION_END,e).emulateTransitionEnd(n)}else e()},e._clearTimeout=function(){clearTimeout(this._timeout),this._timeout=null},t._jQueryInterface=function(e){return this.each((function(){var n=o.default(this),i=n.data(me);if(i||(i=new t(this,"object"==typeof e&&e),n.data(me,i)),"string"==typeof e){if("undefined"==typeof i[e])throw new TypeError('No method named "'+e+'"');i[e](this)}}))},l(t,null,[{key:"VERSION",get:function(){return"4.6.1"}},{key:"DefaultType",get:function(){return Te}},{key:"Default",get:function(){return Ee}}]),t}();o.default.fn.toast=we._jQueryInterface,o.default.fn.toast.Constructor=we,o.default.fn.toast.noConflict=function(){return o.default.fn.toast=pe,we._jQueryInterface},t.Alert=g,t.Button=E,t.Carousel=P,t.Collapse=V,t.Dropdown=lt,t.Modal=Ct,t.Popover=Jt,t.Scrollspy=se,t.Tab=ge,t.Toast=we,t.Tooltip=Wt,t.Util=d,Object.defineProperty(t,"__esModule",{value:!0})})); diff --git a/blade-core-oauth2/src/main/resources/static/js/jquery.min.js b/blade-core-oauth2/src/main/resources/static/js/jquery.min.js new file mode 100644 index 0000000..4d9b3a2 --- /dev/null +++ b/blade-core-oauth2/src/main/resources/static/js/jquery.min.js @@ -0,0 +1,2 @@ +/*! jQuery v3.3.1 | (c) JS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(e,t){"use strict";var n=[],r=e.document,i=Object.getPrototypeOf,o=n.slice,a=n.concat,s=n.push,u=n.indexOf,l={},c=l.toString,f=l.hasOwnProperty,p=f.toString,d=p.call(Object),h={},g=function e(t){return"function"==typeof t&&"number"!=typeof t.nodeType},y=function e(t){return null!=t&&t===t.window},v={type:!0,src:!0,noModule:!0};function m(e,t,n){var i,o=(t=t||r).createElement("script");if(o.text=e,n)for(i in v)n[i]&&(o[i]=n[i]);t.head.appendChild(o).parentNode.removeChild(o)}function x(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?l[c.call(e)]||"object":typeof e}var b="3.3.1",w=function(e,t){return new w.fn.init(e,t)},T=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;w.fn=w.prototype={jquery:"3.3.1",constructor:w,length:0,toArray:function(){return o.call(this)},get:function(e){return null==e?o.call(this):e<0?this[e+this.length]:this[e]},pushStack:function(e){var t=w.merge(this.constructor(),e);return t.prevObject=this,t},each:function(e){return w.each(this,e)},map:function(e){return this.pushStack(w.map(this,function(t,n){return e.call(t,n,t)}))},slice:function(){return this.pushStack(o.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(e<0?t:0);return this.pushStack(n>=0&&n0&&t-1 in e)}var E=function(e){var t,n,r,i,o,a,s,u,l,c,f,p,d,h,g,y,v,m,x,b="sizzle"+1*new Date,w=e.document,T=0,C=0,E=ae(),k=ae(),S=ae(),D=function(e,t){return e===t&&(f=!0),0},N={}.hasOwnProperty,A=[],j=A.pop,q=A.push,L=A.push,H=A.slice,O=function(e,t){for(var n=0,r=e.length;n+~]|"+M+")"+M+"*"),z=new RegExp("="+M+"*([^\\]'\"]*?)"+M+"*\\]","g"),X=new RegExp(W),U=new RegExp("^"+R+"$"),V={ID:new RegExp("^#("+R+")"),CLASS:new RegExp("^\\.("+R+")"),TAG:new RegExp("^("+R+"|[*])"),ATTR:new RegExp("^"+I),PSEUDO:new RegExp("^"+W),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+P+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},G=/^(?:input|select|textarea|button)$/i,Y=/^h\d$/i,Q=/^[^{]+\{\s*\[native \w/,J=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,K=/[+~]/,Z=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),ee=function(e,t,n){var r="0x"+t-65536;return r!==r||n?t:r<0?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)},te=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ne=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},re=function(){p()},ie=me(function(e){return!0===e.disabled&&("form"in e||"label"in e)},{dir:"parentNode",next:"legend"});try{L.apply(A=H.call(w.childNodes),w.childNodes),A[w.childNodes.length].nodeType}catch(e){L={apply:A.length?function(e,t){q.apply(e,H.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function oe(e,t,r,i){var o,s,l,c,f,h,v,m=t&&t.ownerDocument,T=t?t.nodeType:9;if(r=r||[],"string"!=typeof e||!e||1!==T&&9!==T&&11!==T)return r;if(!i&&((t?t.ownerDocument||t:w)!==d&&p(t),t=t||d,g)){if(11!==T&&(f=J.exec(e)))if(o=f[1]){if(9===T){if(!(l=t.getElementById(o)))return r;if(l.id===o)return r.push(l),r}else if(m&&(l=m.getElementById(o))&&x(t,l)&&l.id===o)return r.push(l),r}else{if(f[2])return L.apply(r,t.getElementsByTagName(e)),r;if((o=f[3])&&n.getElementsByClassName&&t.getElementsByClassName)return L.apply(r,t.getElementsByClassName(o)),r}if(n.qsa&&!S[e+" "]&&(!y||!y.test(e))){if(1!==T)m=t,v=e;else if("object"!==t.nodeName.toLowerCase()){(c=t.getAttribute("id"))?c=c.replace(te,ne):t.setAttribute("id",c=b),s=(h=a(e)).length;while(s--)h[s]="#"+c+" "+ve(h[s]);v=h.join(","),m=K.test(e)&&ge(t.parentNode)||t}if(v)try{return L.apply(r,m.querySelectorAll(v)),r}catch(e){}finally{c===b&&t.removeAttribute("id")}}}return u(e.replace(B,"$1"),t,r,i)}function ae(){var e=[];function t(n,i){return e.push(n+" ")>r.cacheLength&&delete t[e.shift()],t[n+" "]=i}return t}function se(e){return e[b]=!0,e}function ue(e){var t=d.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function le(e,t){var n=e.split("|"),i=n.length;while(i--)r.attrHandle[n[i]]=t}function ce(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function fe(e){return function(t){return"input"===t.nodeName.toLowerCase()&&t.type===e}}function pe(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function de(e){return function(t){return"form"in t?t.parentNode&&!1===t.disabled?"label"in t?"label"in t.parentNode?t.parentNode.disabled===e:t.disabled===e:t.isDisabled===e||t.isDisabled!==!e&&ie(t)===e:t.disabled===e:"label"in t&&t.disabled===e}}function he(e){return se(function(t){return t=+t,se(function(n,r){var i,o=e([],n.length,t),a=o.length;while(a--)n[i=o[a]]&&(n[i]=!(r[i]=n[i]))})})}function ge(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}n=oe.support={},o=oe.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return!!t&&"HTML"!==t.nodeName},p=oe.setDocument=function(e){var t,i,a=e?e.ownerDocument||e:w;return a!==d&&9===a.nodeType&&a.documentElement?(d=a,h=d.documentElement,g=!o(d),w!==d&&(i=d.defaultView)&&i.top!==i&&(i.addEventListener?i.addEventListener("unload",re,!1):i.attachEvent&&i.attachEvent("onunload",re)),n.attributes=ue(function(e){return e.className="i",!e.getAttribute("className")}),n.getElementsByTagName=ue(function(e){return e.appendChild(d.createComment("")),!e.getElementsByTagName("*").length}),n.getElementsByClassName=Q.test(d.getElementsByClassName),n.getById=ue(function(e){return h.appendChild(e).id=b,!d.getElementsByName||!d.getElementsByName(b).length}),n.getById?(r.filter.ID=function(e){var t=e.replace(Z,ee);return function(e){return e.getAttribute("id")===t}},r.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&g){var n=t.getElementById(e);return n?[n]:[]}}):(r.filter.ID=function(e){var t=e.replace(Z,ee);return function(e){var n="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return n&&n.value===t}},r.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&g){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),r.find.TAG=n.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):n.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},r.find.CLASS=n.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&g)return t.getElementsByClassName(e)},v=[],y=[],(n.qsa=Q.test(d.querySelectorAll))&&(ue(function(e){h.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&y.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||y.push("\\["+M+"*(?:value|"+P+")"),e.querySelectorAll("[id~="+b+"-]").length||y.push("~="),e.querySelectorAll(":checked").length||y.push(":checked"),e.querySelectorAll("a#"+b+"+*").length||y.push(".#.+[+~]")}),ue(function(e){e.innerHTML="";var t=d.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&y.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&y.push(":enabled",":disabled"),h.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&y.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),y.push(",.*:")})),(n.matchesSelector=Q.test(m=h.matches||h.webkitMatchesSelector||h.mozMatchesSelector||h.oMatchesSelector||h.msMatchesSelector))&&ue(function(e){n.disconnectedMatch=m.call(e,"*"),m.call(e,"[s!='']:x"),v.push("!=",W)}),y=y.length&&new RegExp(y.join("|")),v=v.length&&new RegExp(v.join("|")),t=Q.test(h.compareDocumentPosition),x=t||Q.test(h.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return f=!0,0;var r=!e.compareDocumentPosition-!t.compareDocumentPosition;return r||(1&(r=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!n.sortDetached&&t.compareDocumentPosition(e)===r?e===d||e.ownerDocument===w&&x(w,e)?-1:t===d||t.ownerDocument===w&&x(w,t)?1:c?O(c,e)-O(c,t):0:4&r?-1:1)}:function(e,t){if(e===t)return f=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e===d?-1:t===d?1:i?-1:o?1:c?O(c,e)-O(c,t):0;if(i===o)return ce(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?ce(a[r],s[r]):a[r]===w?-1:s[r]===w?1:0},d):d},oe.matches=function(e,t){return oe(e,null,null,t)},oe.matchesSelector=function(e,t){if((e.ownerDocument||e)!==d&&p(e),t=t.replace(z,"='$1']"),n.matchesSelector&&g&&!S[t+" "]&&(!v||!v.test(t))&&(!y||!y.test(t)))try{var r=m.call(e,t);if(r||n.disconnectedMatch||e.document&&11!==e.document.nodeType)return r}catch(e){}return oe(t,d,null,[e]).length>0},oe.contains=function(e,t){return(e.ownerDocument||e)!==d&&p(e),x(e,t)},oe.attr=function(e,t){(e.ownerDocument||e)!==d&&p(e);var i=r.attrHandle[t.toLowerCase()],o=i&&N.call(r.attrHandle,t.toLowerCase())?i(e,t,!g):void 0;return void 0!==o?o:n.attributes||!g?e.getAttribute(t):(o=e.getAttributeNode(t))&&o.specified?o.value:null},oe.escape=function(e){return(e+"").replace(te,ne)},oe.error=function(e){throw new Error("Syntax error, unrecognized expression: "+e)},oe.uniqueSort=function(e){var t,r=[],i=0,o=0;if(f=!n.detectDuplicates,c=!n.sortStable&&e.slice(0),e.sort(D),f){while(t=e[o++])t===e[o]&&(i=r.push(o));while(i--)e.splice(r[i],1)}return c=null,e},i=oe.getText=function(e){var t,n="",r=0,o=e.nodeType;if(o){if(1===o||9===o||11===o){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=i(e)}else if(3===o||4===o)return e.nodeValue}else while(t=e[r++])n+=i(t);return n},(r=oe.selectors={cacheLength:50,createPseudo:se,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(Z,ee),e[3]=(e[3]||e[4]||e[5]||"").replace(Z,ee),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||oe.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&oe.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return V.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=a(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(Z,ee).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=E[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&E(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r){var i=oe.attr(r,e);return null==i?"!="===t:!t||(i+="","="===t?i===n:"!="===t?i!==n:"^="===t?n&&0===i.indexOf(n):"*="===t?n&&i.indexOf(n)>-1:"$="===t?n&&i.slice(-n.length)===n:"~="===t?(" "+i.replace($," ")+" ").indexOf(n)>-1:"|="===t&&(i===n||i.slice(0,n.length+1)===n+"-"))}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),a="last"!==e.slice(-4),s="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,u){var l,c,f,p,d,h,g=o!==a?"nextSibling":"previousSibling",y=t.parentNode,v=s&&t.nodeName.toLowerCase(),m=!u&&!s,x=!1;if(y){if(o){while(g){p=t;while(p=p[g])if(s?p.nodeName.toLowerCase()===v:1===p.nodeType)return!1;h=g="only"===e&&!h&&"nextSibling"}return!0}if(h=[a?y.firstChild:y.lastChild],a&&m){x=(d=(l=(c=(f=(p=y)[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]||[])[0]===T&&l[1])&&l[2],p=d&&y.childNodes[d];while(p=++d&&p&&p[g]||(x=d=0)||h.pop())if(1===p.nodeType&&++x&&p===t){c[e]=[T,d,x];break}}else if(m&&(x=d=(l=(c=(f=(p=t)[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]||[])[0]===T&&l[1]),!1===x)while(p=++d&&p&&p[g]||(x=d=0)||h.pop())if((s?p.nodeName.toLowerCase()===v:1===p.nodeType)&&++x&&(m&&((c=(f=p[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]=[T,x]),p===t))break;return(x-=i)===r||x%r==0&&x/r>=0}}},PSEUDO:function(e,t){var n,i=r.pseudos[e]||r.setFilters[e.toLowerCase()]||oe.error("unsupported pseudo: "+e);return i[b]?i(t):i.length>1?(n=[e,e,"",t],r.setFilters.hasOwnProperty(e.toLowerCase())?se(function(e,n){var r,o=i(e,t),a=o.length;while(a--)e[r=O(e,o[a])]=!(n[r]=o[a])}):function(e){return i(e,0,n)}):i}},pseudos:{not:se(function(e){var t=[],n=[],r=s(e.replace(B,"$1"));return r[b]?se(function(e,t,n,i){var o,a=r(e,null,i,[]),s=e.length;while(s--)(o=a[s])&&(e[s]=!(t[s]=o))}):function(e,i,o){return t[0]=e,r(t,null,o,n),t[0]=null,!n.pop()}}),has:se(function(e){return function(t){return oe(e,t).length>0}}),contains:se(function(e){return e=e.replace(Z,ee),function(t){return(t.textContent||t.innerText||i(t)).indexOf(e)>-1}}),lang:se(function(e){return U.test(e||"")||oe.error("unsupported lang: "+e),e=e.replace(Z,ee).toLowerCase(),function(t){var n;do{if(n=g?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return(n=n.toLowerCase())===e||0===n.indexOf(e+"-")}while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===h},focus:function(e){return e===d.activeElement&&(!d.hasFocus||d.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:de(!1),disabled:de(!0),checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,!0===e.selected},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeType<6)return!1;return!0},parent:function(e){return!r.pseudos.empty(e)},header:function(e){return Y.test(e.nodeName)},input:function(e){return G.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||"text"===t.toLowerCase())},first:he(function(){return[0]}),last:he(function(e,t){return[t-1]}),eq:he(function(e,t,n){return[n<0?n+t:n]}),even:he(function(e,t){for(var n=0;n=0;)e.push(r);return e}),gt:he(function(e,t,n){for(var r=n<0?n+t:n;++r1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function be(e,t,n){for(var r=0,i=t.length;r-1&&(o[l]=!(a[l]=f))}}else v=we(v===a?v.splice(h,v.length):v),i?i(null,a,v,u):L.apply(a,v)})}function Ce(e){for(var t,n,i,o=e.length,a=r.relative[e[0].type],s=a||r.relative[" "],u=a?1:0,c=me(function(e){return e===t},s,!0),f=me(function(e){return O(t,e)>-1},s,!0),p=[function(e,n,r){var i=!a&&(r||n!==l)||((t=n).nodeType?c(e,n,r):f(e,n,r));return t=null,i}];u1&&xe(p),u>1&&ve(e.slice(0,u-1).concat({value:" "===e[u-2].type?"*":""})).replace(B,"$1"),n,u0,i=e.length>0,o=function(o,a,s,u,c){var f,h,y,v=0,m="0",x=o&&[],b=[],w=l,C=o||i&&r.find.TAG("*",c),E=T+=null==w?1:Math.random()||.1,k=C.length;for(c&&(l=a===d||a||c);m!==k&&null!=(f=C[m]);m++){if(i&&f){h=0,a||f.ownerDocument===d||(p(f),s=!g);while(y=e[h++])if(y(f,a||d,s)){u.push(f);break}c&&(T=E)}n&&((f=!y&&f)&&v--,o&&x.push(f))}if(v+=m,n&&m!==v){h=0;while(y=t[h++])y(x,b,a,s);if(o){if(v>0)while(m--)x[m]||b[m]||(b[m]=j.call(u));b=we(b)}L.apply(u,b),c&&!o&&b.length>0&&v+t.length>1&&oe.uniqueSort(u)}return c&&(T=E,l=w),x};return n?se(o):o}return s=oe.compile=function(e,t){var n,r=[],i=[],o=S[e+" "];if(!o){t||(t=a(e)),n=t.length;while(n--)(o=Ce(t[n]))[b]?r.push(o):i.push(o);(o=S(e,Ee(i,r))).selector=e}return o},u=oe.select=function(e,t,n,i){var o,u,l,c,f,p="function"==typeof e&&e,d=!i&&a(e=p.selector||e);if(n=n||[],1===d.length){if((u=d[0]=d[0].slice(0)).length>2&&"ID"===(l=u[0]).type&&9===t.nodeType&&g&&r.relative[u[1].type]){if(!(t=(r.find.ID(l.matches[0].replace(Z,ee),t)||[])[0]))return n;p&&(t=t.parentNode),e=e.slice(u.shift().value.length)}o=V.needsContext.test(e)?0:u.length;while(o--){if(l=u[o],r.relative[c=l.type])break;if((f=r.find[c])&&(i=f(l.matches[0].replace(Z,ee),K.test(u[0].type)&&ge(t.parentNode)||t))){if(u.splice(o,1),!(e=i.length&&ve(u)))return L.apply(n,i),n;break}}}return(p||s(e,d))(i,t,!g,n,!t||K.test(e)&&ge(t.parentNode)||t),n},n.sortStable=b.split("").sort(D).join("")===b,n.detectDuplicates=!!f,p(),n.sortDetached=ue(function(e){return 1&e.compareDocumentPosition(d.createElement("fieldset"))}),ue(function(e){return e.innerHTML="","#"===e.firstChild.getAttribute("href")})||le("type|href|height|width",function(e,t,n){if(!n)return e.getAttribute(t,"type"===t.toLowerCase()?1:2)}),n.attributes&&ue(function(e){return e.innerHTML="",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||le("value",function(e,t,n){if(!n&&"input"===e.nodeName.toLowerCase())return e.defaultValue}),ue(function(e){return null==e.getAttribute("disabled")})||le(P,function(e,t,n){var r;if(!n)return!0===e[t]?t.toLowerCase():(r=e.getAttributeNode(t))&&r.specified?r.value:null}),oe}(e);w.find=E,w.expr=E.selectors,w.expr[":"]=w.expr.pseudos,w.uniqueSort=w.unique=E.uniqueSort,w.text=E.getText,w.isXMLDoc=E.isXML,w.contains=E.contains,w.escapeSelector=E.escape;var k=function(e,t,n){var r=[],i=void 0!==n;while((e=e[t])&&9!==e.nodeType)if(1===e.nodeType){if(i&&w(e).is(n))break;r.push(e)}return r},S=function(e,t){for(var n=[];e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n},D=w.expr.match.needsContext;function N(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()}var A=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,t,n){return g(t)?w.grep(e,function(e,r){return!!t.call(e,r,e)!==n}):t.nodeType?w.grep(e,function(e){return e===t!==n}):"string"!=typeof t?w.grep(e,function(e){return u.call(t,e)>-1!==n}):w.filter(t,e,n)}w.filter=function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?w.find.matchesSelector(r,e)?[r]:[]:w.find.matches(e,w.grep(t,function(e){return 1===e.nodeType}))},w.fn.extend({find:function(e){var t,n,r=this.length,i=this;if("string"!=typeof e)return this.pushStack(w(e).filter(function(){for(t=0;t1?w.uniqueSort(n):n},filter:function(e){return this.pushStack(j(this,e||[],!1))},not:function(e){return this.pushStack(j(this,e||[],!0))},is:function(e){return!!j(this,"string"==typeof e&&D.test(e)?w(e):e||[],!1).length}});var q,L=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/;(w.fn.init=function(e,t,n){var i,o;if(!e)return this;if(n=n||q,"string"==typeof e){if(!(i="<"===e[0]&&">"===e[e.length-1]&&e.length>=3?[null,e,null]:L.exec(e))||!i[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(i[1]){if(t=t instanceof w?t[0]:t,w.merge(this,w.parseHTML(i[1],t&&t.nodeType?t.ownerDocument||t:r,!0)),A.test(i[1])&&w.isPlainObject(t))for(i in t)g(this[i])?this[i](t[i]):this.attr(i,t[i]);return this}return(o=r.getElementById(i[2]))&&(this[0]=o,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):g(e)?void 0!==n.ready?n.ready(e):e(w):w.makeArray(e,this)}).prototype=w.fn,q=w(r);var H=/^(?:parents|prev(?:Until|All))/,O={children:!0,contents:!0,next:!0,prev:!0};w.fn.extend({has:function(e){var t=w(e,this),n=t.length;return this.filter(function(){for(var e=0;e-1:1===n.nodeType&&w.find.matchesSelector(n,e))){o.push(n);break}return this.pushStack(o.length>1?w.uniqueSort(o):o)},index:function(e){return e?"string"==typeof e?u.call(w(e),this[0]):u.call(this,e.jquery?e[0]:e):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){return this.pushStack(w.uniqueSort(w.merge(this.get(),w(e,t))))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}});function P(e,t){while((e=e[t])&&1!==e.nodeType);return e}w.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return k(e,"parentNode")},parentsUntil:function(e,t,n){return k(e,"parentNode",n)},next:function(e){return P(e,"nextSibling")},prev:function(e){return P(e,"previousSibling")},nextAll:function(e){return k(e,"nextSibling")},prevAll:function(e){return k(e,"previousSibling")},nextUntil:function(e,t,n){return k(e,"nextSibling",n)},prevUntil:function(e,t,n){return k(e,"previousSibling",n)},siblings:function(e){return S((e.parentNode||{}).firstChild,e)},children:function(e){return S(e.firstChild)},contents:function(e){return N(e,"iframe")?e.contentDocument:(N(e,"template")&&(e=e.content||e),w.merge([],e.childNodes))}},function(e,t){w.fn[e]=function(n,r){var i=w.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=w.filter(r,i)),this.length>1&&(O[e]||w.uniqueSort(i),H.test(e)&&i.reverse()),this.pushStack(i)}});var M=/[^\x20\t\r\n\f]+/g;function R(e){var t={};return w.each(e.match(M)||[],function(e,n){t[n]=!0}),t}w.Callbacks=function(e){e="string"==typeof e?R(e):w.extend({},e);var t,n,r,i,o=[],a=[],s=-1,u=function(){for(i=i||e.once,r=t=!0;a.length;s=-1){n=a.shift();while(++s-1)o.splice(n,1),n<=s&&s--}),this},has:function(e){return e?w.inArray(e,o)>-1:o.length>0},empty:function(){return o&&(o=[]),this},disable:function(){return i=a=[],o=n="",this},disabled:function(){return!o},lock:function(){return i=a=[],n||t||(o=n=""),this},locked:function(){return!!i},fireWith:function(e,n){return i||(n=[e,(n=n||[]).slice?n.slice():n],a.push(n),t||u()),this},fire:function(){return l.fireWith(this,arguments),this},fired:function(){return!!r}};return l};function I(e){return e}function W(e){throw e}function $(e,t,n,r){var i;try{e&&g(i=e.promise)?i.call(e).done(t).fail(n):e&&g(i=e.then)?i.call(e,t,n):t.apply(void 0,[e].slice(r))}catch(e){n.apply(void 0,[e])}}w.extend({Deferred:function(t){var n=[["notify","progress",w.Callbacks("memory"),w.Callbacks("memory"),2],["resolve","done",w.Callbacks("once memory"),w.Callbacks("once memory"),0,"resolved"],["reject","fail",w.Callbacks("once memory"),w.Callbacks("once memory"),1,"rejected"]],r="pending",i={state:function(){return r},always:function(){return o.done(arguments).fail(arguments),this},"catch":function(e){return i.then(null,e)},pipe:function(){var e=arguments;return w.Deferred(function(t){w.each(n,function(n,r){var i=g(e[r[4]])&&e[r[4]];o[r[1]](function(){var e=i&&i.apply(this,arguments);e&&g(e.promise)?e.promise().progress(t.notify).done(t.resolve).fail(t.reject):t[r[0]+"With"](this,i?[e]:arguments)})}),e=null}).promise()},then:function(t,r,i){var o=0;function a(t,n,r,i){return function(){var s=this,u=arguments,l=function(){var e,l;if(!(t=o&&(r!==W&&(s=void 0,u=[e]),n.rejectWith(s,u))}};t?c():(w.Deferred.getStackHook&&(c.stackTrace=w.Deferred.getStackHook()),e.setTimeout(c))}}return w.Deferred(function(e){n[0][3].add(a(0,e,g(i)?i:I,e.notifyWith)),n[1][3].add(a(0,e,g(t)?t:I)),n[2][3].add(a(0,e,g(r)?r:W))}).promise()},promise:function(e){return null!=e?w.extend(e,i):i}},o={};return w.each(n,function(e,t){var a=t[2],s=t[5];i[t[1]]=a.add,s&&a.add(function(){r=s},n[3-e][2].disable,n[3-e][3].disable,n[0][2].lock,n[0][3].lock),a.add(t[3].fire),o[t[0]]=function(){return o[t[0]+"With"](this===o?void 0:this,arguments),this},o[t[0]+"With"]=a.fireWith}),i.promise(o),t&&t.call(o,o),o},when:function(e){var t=arguments.length,n=t,r=Array(n),i=o.call(arguments),a=w.Deferred(),s=function(e){return function(n){r[e]=this,i[e]=arguments.length>1?o.call(arguments):n,--t||a.resolveWith(r,i)}};if(t<=1&&($(e,a.done(s(n)).resolve,a.reject,!t),"pending"===a.state()||g(i[n]&&i[n].then)))return a.then();while(n--)$(i[n],s(n),a.reject);return a.promise()}});var B=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;w.Deferred.exceptionHook=function(t,n){e.console&&e.console.warn&&t&&B.test(t.name)&&e.console.warn("jQuery.Deferred exception: "+t.message,t.stack,n)},w.readyException=function(t){e.setTimeout(function(){throw t})};var F=w.Deferred();w.fn.ready=function(e){return F.then(e)["catch"](function(e){w.readyException(e)}),this},w.extend({isReady:!1,readyWait:1,ready:function(e){(!0===e?--w.readyWait:w.isReady)||(w.isReady=!0,!0!==e&&--w.readyWait>0||F.resolveWith(r,[w]))}}),w.ready.then=F.then;function _(){r.removeEventListener("DOMContentLoaded",_),e.removeEventListener("load",_),w.ready()}"complete"===r.readyState||"loading"!==r.readyState&&!r.documentElement.doScroll?e.setTimeout(w.ready):(r.addEventListener("DOMContentLoaded",_),e.addEventListener("load",_));var z=function(e,t,n,r,i,o,a){var s=0,u=e.length,l=null==n;if("object"===x(n)){i=!0;for(s in n)z(e,t,s,n[s],!0,o,a)}else if(void 0!==r&&(i=!0,g(r)||(a=!0),l&&(a?(t.call(e,r),t=null):(l=t,t=function(e,t,n){return l.call(w(e),n)})),t))for(;s1,null,!0)},removeData:function(e){return this.each(function(){K.remove(this,e)})}}),w.extend({queue:function(e,t,n){var r;if(e)return t=(t||"fx")+"queue",r=J.get(e,t),n&&(!r||Array.isArray(n)?r=J.access(e,t,w.makeArray(n)):r.push(n)),r||[]},dequeue:function(e,t){t=t||"fx";var n=w.queue(e,t),r=n.length,i=n.shift(),o=w._queueHooks(e,t),a=function(){w.dequeue(e,t)};"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,a,o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return J.get(e,n)||J.access(e,n,{empty:w.Callbacks("once memory").add(function(){J.remove(e,[t+"queue",n])})})}}),w.fn.extend({queue:function(e,t){var n=2;return"string"!=typeof e&&(t=e,e="fx",n--),arguments.length\x20\t\r\n\f]+)/i,he=/^$|^module$|\/(?:java|ecma)script/i,ge={option:[1,""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};ge.optgroup=ge.option,ge.tbody=ge.tfoot=ge.colgroup=ge.caption=ge.thead,ge.th=ge.td;function ye(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&N(e,t)?w.merge([e],n):n}function ve(e,t){for(var n=0,r=e.length;n-1)i&&i.push(o);else if(l=w.contains(o.ownerDocument,o),a=ye(f.appendChild(o),"script"),l&&ve(a),n){c=0;while(o=a[c++])he.test(o.type||"")&&n.push(o)}return f}!function(){var e=r.createDocumentFragment().appendChild(r.createElement("div")),t=r.createElement("input");t.setAttribute("type","radio"),t.setAttribute("checked","checked"),t.setAttribute("name","t"),e.appendChild(t),h.checkClone=e.cloneNode(!0).cloneNode(!0).lastChild.checked,e.innerHTML="",h.noCloneChecked=!!e.cloneNode(!0).lastChild.defaultValue}();var be=r.documentElement,we=/^key/,Te=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,Ce=/^([^.]*)(?:\.(.+)|)/;function Ee(){return!0}function ke(){return!1}function Se(){try{return r.activeElement}catch(e){}}function De(e,t,n,r,i,o){var a,s;if("object"==typeof t){"string"!=typeof n&&(r=r||n,n=void 0);for(s in t)De(e,s,n,r,t[s],o);return e}if(null==r&&null==i?(i=n,r=n=void 0):null==i&&("string"==typeof n?(i=r,r=void 0):(i=r,r=n,n=void 0)),!1===i)i=ke;else if(!i)return e;return 1===o&&(a=i,(i=function(e){return w().off(e),a.apply(this,arguments)}).guid=a.guid||(a.guid=w.guid++)),e.each(function(){w.event.add(this,t,i,r,n)})}w.event={global:{},add:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,y=J.get(e);if(y){n.handler&&(n=(o=n).handler,i=o.selector),i&&w.find.matchesSelector(be,i),n.guid||(n.guid=w.guid++),(u=y.events)||(u=y.events={}),(a=y.handle)||(a=y.handle=function(t){return"undefined"!=typeof w&&w.event.triggered!==t.type?w.event.dispatch.apply(e,arguments):void 0}),l=(t=(t||"").match(M)||[""]).length;while(l--)d=g=(s=Ce.exec(t[l])||[])[1],h=(s[2]||"").split(".").sort(),d&&(f=w.event.special[d]||{},d=(i?f.delegateType:f.bindType)||d,f=w.event.special[d]||{},c=w.extend({type:d,origType:g,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&w.expr.match.needsContext.test(i),namespace:h.join(".")},o),(p=u[d])||((p=u[d]=[]).delegateCount=0,f.setup&&!1!==f.setup.call(e,r,h,a)||e.addEventListener&&e.addEventListener(d,a)),f.add&&(f.add.call(e,c),c.handler.guid||(c.handler.guid=n.guid)),i?p.splice(p.delegateCount++,0,c):p.push(c),w.event.global[d]=!0)}},remove:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,y=J.hasData(e)&&J.get(e);if(y&&(u=y.events)){l=(t=(t||"").match(M)||[""]).length;while(l--)if(s=Ce.exec(t[l])||[],d=g=s[1],h=(s[2]||"").split(".").sort(),d){f=w.event.special[d]||{},p=u[d=(r?f.delegateType:f.bindType)||d]||[],s=s[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),a=o=p.length;while(o--)c=p[o],!i&&g!==c.origType||n&&n.guid!==c.guid||s&&!s.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(p.splice(o,1),c.selector&&p.delegateCount--,f.remove&&f.remove.call(e,c));a&&!p.length&&(f.teardown&&!1!==f.teardown.call(e,h,y.handle)||w.removeEvent(e,d,y.handle),delete u[d])}else for(d in u)w.event.remove(e,d+t[l],n,r,!0);w.isEmptyObject(u)&&J.remove(e,"handle events")}},dispatch:function(e){var t=w.event.fix(e),n,r,i,o,a,s,u=new Array(arguments.length),l=(J.get(this,"events")||{})[t.type]||[],c=w.event.special[t.type]||{};for(u[0]=t,n=1;n=1))for(;l!==this;l=l.parentNode||this)if(1===l.nodeType&&("click"!==e.type||!0!==l.disabled)){for(o=[],a={},n=0;n-1:w.find(i,this,null,[l]).length),a[i]&&o.push(r);o.length&&s.push({elem:l,handlers:o})}return l=this,u\x20\t\r\n\f]*)[^>]*)\/>/gi,Ae=/\s*$/g;function Le(e,t){return N(e,"table")&&N(11!==t.nodeType?t:t.firstChild,"tr")?w(e).children("tbody")[0]||e:e}function He(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function Oe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Pe(e,t){var n,r,i,o,a,s,u,l;if(1===t.nodeType){if(J.hasData(e)&&(o=J.access(e),a=J.set(t,o),l=o.events)){delete a.handle,a.events={};for(i in l)for(n=0,r=l[i].length;n1&&"string"==typeof y&&!h.checkClone&&je.test(y))return e.each(function(i){var o=e.eq(i);v&&(t[0]=y.call(this,i,o.html())),Re(o,t,n,r)});if(p&&(i=xe(t,e[0].ownerDocument,!1,e,r),o=i.firstChild,1===i.childNodes.length&&(i=o),o||r)){for(u=(s=w.map(ye(i,"script"),He)).length;f")},clone:function(e,t,n){var r,i,o,a,s=e.cloneNode(!0),u=w.contains(e.ownerDocument,e);if(!(h.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||w.isXMLDoc(e)))for(a=ye(s),r=0,i=(o=ye(e)).length;r0&&ve(a,!u&&ye(e,"script")),s},cleanData:function(e){for(var t,n,r,i=w.event.special,o=0;void 0!==(n=e[o]);o++)if(Y(n)){if(t=n[J.expando]){if(t.events)for(r in t.events)i[r]?w.event.remove(n,r):w.removeEvent(n,r,t.handle);n[J.expando]=void 0}n[K.expando]&&(n[K.expando]=void 0)}}}),w.fn.extend({detach:function(e){return Ie(this,e,!0)},remove:function(e){return Ie(this,e)},text:function(e){return z(this,function(e){return void 0===e?w.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=e)})},null,e,arguments.length)},append:function(){return Re(this,arguments,function(e){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||Le(this,e).appendChild(e)})},prepend:function(){return Re(this,arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Le(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return Re(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return Re(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},empty:function(){for(var e,t=0;null!=(e=this[t]);t++)1===e.nodeType&&(w.cleanData(ye(e,!1)),e.textContent="");return this},clone:function(e,t){return e=null!=e&&e,t=null==t?e:t,this.map(function(){return w.clone(this,e,t)})},html:function(e){return z(this,function(e){var t=this[0]||{},n=0,r=this.length;if(void 0===e&&1===t.nodeType)return t.innerHTML;if("string"==typeof e&&!Ae.test(e)&&!ge[(de.exec(e)||["",""])[1].toLowerCase()]){e=w.htmlPrefilter(e);try{for(;n=0&&(u+=Math.max(0,Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-o-u-s-.5))),u}function et(e,t,n){var r=$e(e),i=Fe(e,t,r),o="border-box"===w.css(e,"boxSizing",!1,r),a=o;if(We.test(i)){if(!n)return i;i="auto"}return a=a&&(h.boxSizingReliable()||i===e.style[t]),("auto"===i||!parseFloat(i)&&"inline"===w.css(e,"display",!1,r))&&(i=e["offset"+t[0].toUpperCase()+t.slice(1)],a=!0),(i=parseFloat(i)||0)+Ze(e,t,n||(o?"border":"content"),a,r,i)+"px"}w.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=Fe(e,"opacity");return""===n?"1":n}}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{},style:function(e,t,n,r){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var i,o,a,s=G(t),u=Xe.test(t),l=e.style;if(u||(t=Je(s)),a=w.cssHooks[t]||w.cssHooks[s],void 0===n)return a&&"get"in a&&void 0!==(i=a.get(e,!1,r))?i:l[t];"string"==(o=typeof n)&&(i=ie.exec(n))&&i[1]&&(n=ue(e,t,i),o="number"),null!=n&&n===n&&("number"===o&&(n+=i&&i[3]||(w.cssNumber[s]?"":"px")),h.clearCloneStyle||""!==n||0!==t.indexOf("background")||(l[t]="inherit"),a&&"set"in a&&void 0===(n=a.set(e,n,r))||(u?l.setProperty(t,n):l[t]=n))}},css:function(e,t,n,r){var i,o,a,s=G(t);return Xe.test(t)||(t=Je(s)),(a=w.cssHooks[t]||w.cssHooks[s])&&"get"in a&&(i=a.get(e,!0,n)),void 0===i&&(i=Fe(e,t,r)),"normal"===i&&t in Ve&&(i=Ve[t]),""===n||n?(o=parseFloat(i),!0===n||isFinite(o)?o||0:i):i}}),w.each(["height","width"],function(e,t){w.cssHooks[t]={get:function(e,n,r){if(n)return!ze.test(w.css(e,"display"))||e.getClientRects().length&&e.getBoundingClientRect().width?et(e,t,r):se(e,Ue,function(){return et(e,t,r)})},set:function(e,n,r){var i,o=$e(e),a="border-box"===w.css(e,"boxSizing",!1,o),s=r&&Ze(e,t,r,a,o);return a&&h.scrollboxSize()===o.position&&(s-=Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-parseFloat(o[t])-Ze(e,t,"border",!1,o)-.5)),s&&(i=ie.exec(n))&&"px"!==(i[3]||"px")&&(e.style[t]=n,n=w.css(e,t)),Ke(e,n,s)}}}),w.cssHooks.marginLeft=_e(h.reliableMarginLeft,function(e,t){if(t)return(parseFloat(Fe(e,"marginLeft"))||e.getBoundingClientRect().left-se(e,{marginLeft:0},function(){return e.getBoundingClientRect().left}))+"px"}),w.each({margin:"",padding:"",border:"Width"},function(e,t){w.cssHooks[e+t]={expand:function(n){for(var r=0,i={},o="string"==typeof n?n.split(" "):[n];r<4;r++)i[e+oe[r]+t]=o[r]||o[r-2]||o[0];return i}},"margin"!==e&&(w.cssHooks[e+t].set=Ke)}),w.fn.extend({css:function(e,t){return z(this,function(e,t,n){var r,i,o={},a=0;if(Array.isArray(t)){for(r=$e(e),i=t.length;a1)}});function tt(e,t,n,r,i){return new tt.prototype.init(e,t,n,r,i)}w.Tween=tt,tt.prototype={constructor:tt,init:function(e,t,n,r,i,o){this.elem=e,this.prop=n,this.easing=i||w.easing._default,this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=o||(w.cssNumber[n]?"":"px")},cur:function(){var e=tt.propHooks[this.prop];return e&&e.get?e.get(this):tt.propHooks._default.get(this)},run:function(e){var t,n=tt.propHooks[this.prop];return this.options.duration?this.pos=t=w.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):this.pos=t=e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):tt.propHooks._default.set(this),this}},tt.prototype.init.prototype=tt.prototype,tt.propHooks={_default:{get:function(e){var t;return 1!==e.elem.nodeType||null!=e.elem[e.prop]&&null==e.elem.style[e.prop]?e.elem[e.prop]:(t=w.css(e.elem,e.prop,""))&&"auto"!==t?t:0},set:function(e){w.fx.step[e.prop]?w.fx.step[e.prop](e):1!==e.elem.nodeType||null==e.elem.style[w.cssProps[e.prop]]&&!w.cssHooks[e.prop]?e.elem[e.prop]=e.now:w.style(e.elem,e.prop,e.now+e.unit)}}},tt.propHooks.scrollTop=tt.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},w.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2},_default:"swing"},w.fx=tt.prototype.init,w.fx.step={};var nt,rt,it=/^(?:toggle|show|hide)$/,ot=/queueHooks$/;function at(){rt&&(!1===r.hidden&&e.requestAnimationFrame?e.requestAnimationFrame(at):e.setTimeout(at,w.fx.interval),w.fx.tick())}function st(){return e.setTimeout(function(){nt=void 0}),nt=Date.now()}function ut(e,t){var n,r=0,i={height:e};for(t=t?1:0;r<4;r+=2-t)i["margin"+(n=oe[r])]=i["padding"+n]=e;return t&&(i.opacity=i.width=e),i}function lt(e,t,n){for(var r,i=(pt.tweeners[t]||[]).concat(pt.tweeners["*"]),o=0,a=i.length;o1)},removeAttr:function(e){return this.each(function(){w.removeAttr(this,e)})}}),w.extend({attr:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return"undefined"==typeof e.getAttribute?w.prop(e,t,n):(1===o&&w.isXMLDoc(e)||(i=w.attrHooks[t.toLowerCase()]||(w.expr.match.bool.test(t)?dt:void 0)),void 0!==n?null===n?void w.removeAttr(e,t):i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:(e.setAttribute(t,n+""),n):i&&"get"in i&&null!==(r=i.get(e,t))?r:null==(r=w.find.attr(e,t))?void 0:r)},attrHooks:{type:{set:function(e,t){if(!h.radioValue&&"radio"===t&&N(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},removeAttr:function(e,t){var n,r=0,i=t&&t.match(M);if(i&&1===e.nodeType)while(n=i[r++])e.removeAttribute(n)}}),dt={set:function(e,t,n){return!1===t?w.removeAttr(e,n):e.setAttribute(n,n),n}},w.each(w.expr.match.bool.source.match(/\w+/g),function(e,t){var n=ht[t]||w.find.attr;ht[t]=function(e,t,r){var i,o,a=t.toLowerCase();return r||(o=ht[a],ht[a]=i,i=null!=n(e,t,r)?a:null,ht[a]=o),i}});var gt=/^(?:input|select|textarea|button)$/i,yt=/^(?:a|area)$/i;w.fn.extend({prop:function(e,t){return z(this,w.prop,e,t,arguments.length>1)},removeProp:function(e){return this.each(function(){delete this[w.propFix[e]||e]})}}),w.extend({prop:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return 1===o&&w.isXMLDoc(e)||(t=w.propFix[t]||t,i=w.propHooks[t]),void 0!==n?i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:e[t]=n:i&&"get"in i&&null!==(r=i.get(e,t))?r:e[t]},propHooks:{tabIndex:{get:function(e){var t=w.find.attr(e,"tabindex");return t?parseInt(t,10):gt.test(e.nodeName)||yt.test(e.nodeName)&&e.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),h.optSelected||(w.propHooks.selected={get:function(e){var t=e.parentNode;return t&&t.parentNode&&t.parentNode.selectedIndex,null},set:function(e){var t=e.parentNode;t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex)}}),w.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){w.propFix[this.toLowerCase()]=this});function vt(e){return(e.match(M)||[]).join(" ")}function mt(e){return e.getAttribute&&e.getAttribute("class")||""}function xt(e){return Array.isArray(e)?e:"string"==typeof e?e.match(M)||[]:[]}w.fn.extend({addClass:function(e){var t,n,r,i,o,a,s,u=0;if(g(e))return this.each(function(t){w(this).addClass(e.call(this,t,mt(this)))});if((t=xt(e)).length)while(n=this[u++])if(i=mt(n),r=1===n.nodeType&&" "+vt(i)+" "){a=0;while(o=t[a++])r.indexOf(" "+o+" ")<0&&(r+=o+" ");i!==(s=vt(r))&&n.setAttribute("class",s)}return this},removeClass:function(e){var t,n,r,i,o,a,s,u=0;if(g(e))return this.each(function(t){w(this).removeClass(e.call(this,t,mt(this)))});if(!arguments.length)return this.attr("class","");if((t=xt(e)).length)while(n=this[u++])if(i=mt(n),r=1===n.nodeType&&" "+vt(i)+" "){a=0;while(o=t[a++])while(r.indexOf(" "+o+" ")>-1)r=r.replace(" "+o+" "," ");i!==(s=vt(r))&&n.setAttribute("class",s)}return this},toggleClass:function(e,t){var n=typeof e,r="string"===n||Array.isArray(e);return"boolean"==typeof t&&r?t?this.addClass(e):this.removeClass(e):g(e)?this.each(function(n){w(this).toggleClass(e.call(this,n,mt(this),t),t)}):this.each(function(){var t,i,o,a;if(r){i=0,o=w(this),a=xt(e);while(t=a[i++])o.hasClass(t)?o.removeClass(t):o.addClass(t)}else void 0!==e&&"boolean"!==n||((t=mt(this))&&J.set(this,"__className__",t),this.setAttribute&&this.setAttribute("class",t||!1===e?"":J.get(this,"__className__")||""))})},hasClass:function(e){var t,n,r=0;t=" "+e+" ";while(n=this[r++])if(1===n.nodeType&&(" "+vt(mt(n))+" ").indexOf(t)>-1)return!0;return!1}});var bt=/\r/g;w.fn.extend({val:function(e){var t,n,r,i=this[0];{if(arguments.length)return r=g(e),this.each(function(n){var i;1===this.nodeType&&(null==(i=r?e.call(this,n,w(this).val()):e)?i="":"number"==typeof i?i+="":Array.isArray(i)&&(i=w.map(i,function(e){return null==e?"":e+""})),(t=w.valHooks[this.type]||w.valHooks[this.nodeName.toLowerCase()])&&"set"in t&&void 0!==t.set(this,i,"value")||(this.value=i))});if(i)return(t=w.valHooks[i.type]||w.valHooks[i.nodeName.toLowerCase()])&&"get"in t&&void 0!==(n=t.get(i,"value"))?n:"string"==typeof(n=i.value)?n.replace(bt,""):null==n?"":n}}}),w.extend({valHooks:{option:{get:function(e){var t=w.find.attr(e,"value");return null!=t?t:vt(w.text(e))}},select:{get:function(e){var t,n,r,i=e.options,o=e.selectedIndex,a="select-one"===e.type,s=a?null:[],u=a?o+1:i.length;for(r=o<0?u:a?o:0;r-1)&&(n=!0);return n||(e.selectedIndex=-1),o}}}}),w.each(["radio","checkbox"],function(){w.valHooks[this]={set:function(e,t){if(Array.isArray(t))return e.checked=w.inArray(w(e).val(),t)>-1}},h.checkOn||(w.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})}),h.focusin="onfocusin"in e;var wt=/^(?:focusinfocus|focusoutblur)$/,Tt=function(e){e.stopPropagation()};w.extend(w.event,{trigger:function(t,n,i,o){var a,s,u,l,c,p,d,h,v=[i||r],m=f.call(t,"type")?t.type:t,x=f.call(t,"namespace")?t.namespace.split("."):[];if(s=h=u=i=i||r,3!==i.nodeType&&8!==i.nodeType&&!wt.test(m+w.event.triggered)&&(m.indexOf(".")>-1&&(m=(x=m.split(".")).shift(),x.sort()),c=m.indexOf(":")<0&&"on"+m,t=t[w.expando]?t:new w.Event(m,"object"==typeof t&&t),t.isTrigger=o?2:3,t.namespace=x.join("."),t.rnamespace=t.namespace?new RegExp("(^|\\.)"+x.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,t.result=void 0,t.target||(t.target=i),n=null==n?[t]:w.makeArray(n,[t]),d=w.event.special[m]||{},o||!d.trigger||!1!==d.trigger.apply(i,n))){if(!o&&!d.noBubble&&!y(i)){for(l=d.delegateType||m,wt.test(l+m)||(s=s.parentNode);s;s=s.parentNode)v.push(s),u=s;u===(i.ownerDocument||r)&&v.push(u.defaultView||u.parentWindow||e)}a=0;while((s=v[a++])&&!t.isPropagationStopped())h=s,t.type=a>1?l:d.bindType||m,(p=(J.get(s,"events")||{})[t.type]&&J.get(s,"handle"))&&p.apply(s,n),(p=c&&s[c])&&p.apply&&Y(s)&&(t.result=p.apply(s,n),!1===t.result&&t.preventDefault());return t.type=m,o||t.isDefaultPrevented()||d._default&&!1!==d._default.apply(v.pop(),n)||!Y(i)||c&&g(i[m])&&!y(i)&&((u=i[c])&&(i[c]=null),w.event.triggered=m,t.isPropagationStopped()&&h.addEventListener(m,Tt),i[m](),t.isPropagationStopped()&&h.removeEventListener(m,Tt),w.event.triggered=void 0,u&&(i[c]=u)),t.result}},simulate:function(e,t,n){var r=w.extend(new w.Event,n,{type:e,isSimulated:!0});w.event.trigger(r,null,t)}}),w.fn.extend({trigger:function(e,t){return this.each(function(){w.event.trigger(e,t,this)})},triggerHandler:function(e,t){var n=this[0];if(n)return w.event.trigger(e,t,n,!0)}}),h.focusin||w.each({focus:"focusin",blur:"focusout"},function(e,t){var n=function(e){w.event.simulate(t,e.target,w.event.fix(e))};w.event.special[t]={setup:function(){var r=this.ownerDocument||this,i=J.access(r,t);i||r.addEventListener(e,n,!0),J.access(r,t,(i||0)+1)},teardown:function(){var r=this.ownerDocument||this,i=J.access(r,t)-1;i?J.access(r,t,i):(r.removeEventListener(e,n,!0),J.remove(r,t))}}});var Ct=e.location,Et=Date.now(),kt=/\?/;w.parseXML=function(t){var n;if(!t||"string"!=typeof t)return null;try{n=(new e.DOMParser).parseFromString(t,"text/xml")}catch(e){n=void 0}return n&&!n.getElementsByTagName("parsererror").length||w.error("Invalid XML: "+t),n};var St=/\[\]$/,Dt=/\r?\n/g,Nt=/^(?:submit|button|image|reset|file)$/i,At=/^(?:input|select|textarea|keygen)/i;function jt(e,t,n,r){var i;if(Array.isArray(t))w.each(t,function(t,i){n||St.test(e)?r(e,i):jt(e+"["+("object"==typeof i&&null!=i?t:"")+"]",i,n,r)});else if(n||"object"!==x(t))r(e,t);else for(i in t)jt(e+"["+i+"]",t[i],n,r)}w.param=function(e,t){var n,r=[],i=function(e,t){var n=g(t)?t():t;r[r.length]=encodeURIComponent(e)+"="+encodeURIComponent(null==n?"":n)};if(Array.isArray(e)||e.jquery&&!w.isPlainObject(e))w.each(e,function(){i(this.name,this.value)});else for(n in e)jt(n,e[n],t,i);return r.join("&")},w.fn.extend({serialize:function(){return w.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=w.prop(this,"elements");return e?w.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!w(this).is(":disabled")&&At.test(this.nodeName)&&!Nt.test(e)&&(this.checked||!pe.test(e))}).map(function(e,t){var n=w(this).val();return null==n?null:Array.isArray(n)?w.map(n,function(e){return{name:t.name,value:e.replace(Dt,"\r\n")}}):{name:t.name,value:n.replace(Dt,"\r\n")}}).get()}});var qt=/%20/g,Lt=/#.*$/,Ht=/([?&])_=[^&]*/,Ot=/^(.*?):[ \t]*([^\r\n]*)$/gm,Pt=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Mt=/^(?:GET|HEAD)$/,Rt=/^\/\//,It={},Wt={},$t="*/".concat("*"),Bt=r.createElement("a");Bt.href=Ct.href;function Ft(e){return function(t,n){"string"!=typeof t&&(n=t,t="*");var r,i=0,o=t.toLowerCase().match(M)||[];if(g(n))while(r=o[i++])"+"===r[0]?(r=r.slice(1)||"*",(e[r]=e[r]||[]).unshift(n)):(e[r]=e[r]||[]).push(n)}}function _t(e,t,n,r){var i={},o=e===Wt;function a(s){var u;return i[s]=!0,w.each(e[s]||[],function(e,s){var l=s(t,n,r);return"string"!=typeof l||o||i[l]?o?!(u=l):void 0:(t.dataTypes.unshift(l),a(l),!1)}),u}return a(t.dataTypes[0])||!i["*"]&&a("*")}function zt(e,t){var n,r,i=w.ajaxSettings.flatOptions||{};for(n in t)void 0!==t[n]&&((i[n]?e:r||(r={}))[n]=t[n]);return r&&w.extend(!0,e,r),e}function Xt(e,t,n){var r,i,o,a,s=e.contents,u=e.dataTypes;while("*"===u[0])u.shift(),void 0===r&&(r=e.mimeType||t.getResponseHeader("Content-Type"));if(r)for(i in s)if(s[i]&&s[i].test(r)){u.unshift(i);break}if(u[0]in n)o=u[0];else{for(i in n){if(!u[0]||e.converters[i+" "+u[0]]){o=i;break}a||(a=i)}o=o||a}if(o)return o!==u[0]&&u.unshift(o),n[o]}function Ut(e,t,n,r){var i,o,a,s,u,l={},c=e.dataTypes.slice();if(c[1])for(a in e.converters)l[a.toLowerCase()]=e.converters[a];o=c.shift();while(o)if(e.responseFields[o]&&(n[e.responseFields[o]]=t),!u&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),u=o,o=c.shift())if("*"===o)o=u;else if("*"!==u&&u!==o){if(!(a=l[u+" "+o]||l["* "+o]))for(i in l)if((s=i.split(" "))[1]===o&&(a=l[u+" "+s[0]]||l["* "+s[0]])){!0===a?a=l[i]:!0!==l[i]&&(o=s[0],c.unshift(s[1]));break}if(!0!==a)if(a&&e["throws"])t=a(t);else try{t=a(t)}catch(e){return{state:"parsererror",error:a?e:"No conversion from "+u+" to "+o}}}return{state:"success",data:t}}w.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:Ct.href,type:"GET",isLocal:Pt.test(Ct.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":$t,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":w.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?zt(zt(e,w.ajaxSettings),t):zt(w.ajaxSettings,e)},ajaxPrefilter:Ft(It),ajaxTransport:Ft(Wt),ajax:function(t,n){"object"==typeof t&&(n=t,t=void 0),n=n||{};var i,o,a,s,u,l,c,f,p,d,h=w.ajaxSetup({},n),g=h.context||h,y=h.context&&(g.nodeType||g.jquery)?w(g):w.event,v=w.Deferred(),m=w.Callbacks("once memory"),x=h.statusCode||{},b={},T={},C="canceled",E={readyState:0,getResponseHeader:function(e){var t;if(c){if(!s){s={};while(t=Ot.exec(a))s[t[1].toLowerCase()]=t[2]}t=s[e.toLowerCase()]}return null==t?null:t},getAllResponseHeaders:function(){return c?a:null},setRequestHeader:function(e,t){return null==c&&(e=T[e.toLowerCase()]=T[e.toLowerCase()]||e,b[e]=t),this},overrideMimeType:function(e){return null==c&&(h.mimeType=e),this},statusCode:function(e){var t;if(e)if(c)E.always(e[E.status]);else for(t in e)x[t]=[x[t],e[t]];return this},abort:function(e){var t=e||C;return i&&i.abort(t),k(0,t),this}};if(v.promise(E),h.url=((t||h.url||Ct.href)+"").replace(Rt,Ct.protocol+"//"),h.type=n.method||n.type||h.method||h.type,h.dataTypes=(h.dataType||"*").toLowerCase().match(M)||[""],null==h.crossDomain){l=r.createElement("a");try{l.href=h.url,l.href=l.href,h.crossDomain=Bt.protocol+"//"+Bt.host!=l.protocol+"//"+l.host}catch(e){h.crossDomain=!0}}if(h.data&&h.processData&&"string"!=typeof h.data&&(h.data=w.param(h.data,h.traditional)),_t(It,h,n,E),c)return E;(f=w.event&&h.global)&&0==w.active++&&w.event.trigger("ajaxStart"),h.type=h.type.toUpperCase(),h.hasContent=!Mt.test(h.type),o=h.url.replace(Lt,""),h.hasContent?h.data&&h.processData&&0===(h.contentType||"").indexOf("application/x-www-form-urlencoded")&&(h.data=h.data.replace(qt,"+")):(d=h.url.slice(o.length),h.data&&(h.processData||"string"==typeof h.data)&&(o+=(kt.test(o)?"&":"?")+h.data,delete h.data),!1===h.cache&&(o=o.replace(Ht,"$1"),d=(kt.test(o)?"&":"?")+"_="+Et+++d),h.url=o+d),h.ifModified&&(w.lastModified[o]&&E.setRequestHeader("If-Modified-Since",w.lastModified[o]),w.etag[o]&&E.setRequestHeader("If-None-Match",w.etag[o])),(h.data&&h.hasContent&&!1!==h.contentType||n.contentType)&&E.setRequestHeader("Content-Type",h.contentType),E.setRequestHeader("Accept",h.dataTypes[0]&&h.accepts[h.dataTypes[0]]?h.accepts[h.dataTypes[0]]+("*"!==h.dataTypes[0]?", "+$t+"; q=0.01":""):h.accepts["*"]);for(p in h.headers)E.setRequestHeader(p,h.headers[p]);if(h.beforeSend&&(!1===h.beforeSend.call(g,E,h)||c))return E.abort();if(C="abort",m.add(h.complete),E.done(h.success),E.fail(h.error),i=_t(Wt,h,n,E)){if(E.readyState=1,f&&y.trigger("ajaxSend",[E,h]),c)return E;h.async&&h.timeout>0&&(u=e.setTimeout(function(){E.abort("timeout")},h.timeout));try{c=!1,i.send(b,k)}catch(e){if(c)throw e;k(-1,e)}}else k(-1,"No Transport");function k(t,n,r,s){var l,p,d,b,T,C=n;c||(c=!0,u&&e.clearTimeout(u),i=void 0,a=s||"",E.readyState=t>0?4:0,l=t>=200&&t<300||304===t,r&&(b=Xt(h,E,r)),b=Ut(h,b,E,l),l?(h.ifModified&&((T=E.getResponseHeader("Last-Modified"))&&(w.lastModified[o]=T),(T=E.getResponseHeader("etag"))&&(w.etag[o]=T)),204===t||"HEAD"===h.type?C="nocontent":304===t?C="notmodified":(C=b.state,p=b.data,l=!(d=b.error))):(d=C,!t&&C||(C="error",t<0&&(t=0))),E.status=t,E.statusText=(n||C)+"",l?v.resolveWith(g,[p,C,E]):v.rejectWith(g,[E,C,d]),E.statusCode(x),x=void 0,f&&y.trigger(l?"ajaxSuccess":"ajaxError",[E,h,l?p:d]),m.fireWith(g,[E,C]),f&&(y.trigger("ajaxComplete",[E,h]),--w.active||w.event.trigger("ajaxStop")))}return E},getJSON:function(e,t,n){return w.get(e,t,n,"json")},getScript:function(e,t){return w.get(e,void 0,t,"script")}}),w.each(["get","post"],function(e,t){w[t]=function(e,n,r,i){return g(n)&&(i=i||r,r=n,n=void 0),w.ajax(w.extend({url:e,type:t,dataType:i,data:n,success:r},w.isPlainObject(e)&&e))}}),w._evalUrl=function(e){return w.ajax({url:e,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},w.fn.extend({wrapAll:function(e){var t;return this[0]&&(g(e)&&(e=e.call(this[0])),t=w(e,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstElementChild)e=e.firstElementChild;return e}).append(this)),this},wrapInner:function(e){return g(e)?this.each(function(t){w(this).wrapInner(e.call(this,t))}):this.each(function(){var t=w(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=g(e);return this.each(function(n){w(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(e){return this.parent(e).not("body").each(function(){w(this).replaceWith(this.childNodes)}),this}}),w.expr.pseudos.hidden=function(e){return!w.expr.pseudos.visible(e)},w.expr.pseudos.visible=function(e){return!!(e.offsetWidth||e.offsetHeight||e.getClientRects().length)},w.ajaxSettings.xhr=function(){try{return new e.XMLHttpRequest}catch(e){}};var Vt={0:200,1223:204},Gt=w.ajaxSettings.xhr();h.cors=!!Gt&&"withCredentials"in Gt,h.ajax=Gt=!!Gt,w.ajaxTransport(function(t){var n,r;if(h.cors||Gt&&!t.crossDomain)return{send:function(i,o){var a,s=t.xhr();if(s.open(t.type,t.url,t.async,t.username,t.password),t.xhrFields)for(a in t.xhrFields)s[a]=t.xhrFields[a];t.mimeType&&s.overrideMimeType&&s.overrideMimeType(t.mimeType),t.crossDomain||i["X-Requested-With"]||(i["X-Requested-With"]="XMLHttpRequest");for(a in i)s.setRequestHeader(a,i[a]);n=function(e){return function(){n&&(n=r=s.onload=s.onerror=s.onabort=s.ontimeout=s.onreadystatechange=null,"abort"===e?s.abort():"error"===e?"number"!=typeof s.status?o(0,"error"):o(s.status,s.statusText):o(Vt[s.status]||s.status,s.statusText,"text"!==(s.responseType||"text")||"string"!=typeof s.responseText?{binary:s.response}:{text:s.responseText},s.getAllResponseHeaders()))}},s.onload=n(),r=s.onerror=s.ontimeout=n("error"),void 0!==s.onabort?s.onabort=r:s.onreadystatechange=function(){4===s.readyState&&e.setTimeout(function(){n&&r()})},n=n("abort");try{s.send(t.hasContent&&t.data||null)}catch(e){if(n)throw e}},abort:function(){n&&n()}}}),w.ajaxPrefilter(function(e){e.crossDomain&&(e.contents.script=!1)}),w.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(e){return w.globalEval(e),e}}}),w.ajaxPrefilter("script",function(e){void 0===e.cache&&(e.cache=!1),e.crossDomain&&(e.type="GET")}),w.ajaxTransport("script",function(e){if(e.crossDomain){var t,n;return{send:function(i,o){t=w(" + + + + + diff --git a/blade-core-oauth2/src/main/resources/templates/error.html b/blade-core-oauth2/src/main/resources/templates/error.html new file mode 100644 index 0000000..77ddf6a --- /dev/null +++ b/blade-core-oauth2/src/main/resources/templates/error.html @@ -0,0 +1,45 @@ + + + + + + BladeX 统一认证系统 + + + + + +
+
+
+
+
+
+ +

BladeX 统一认证系统

+
+

应用授权失败

+ +
+ +
+
+ +
+
+
+
+ + + + + + diff --git a/blade-core-oauth2/src/main/resources/templates/login.html b/blade-core-oauth2/src/main/resources/templates/login.html new file mode 100644 index 0000000..27f3c63 --- /dev/null +++ b/blade-core-oauth2/src/main/resources/templates/login.html @@ -0,0 +1,55 @@ + + + + + + BladeX 统一认证系统 + + + + + +
+
+
+
+
+
+ +

BladeX 统一认证系统

+
+

欢迎使用统一认证,提交后请对应用进行授权

+ +
+ + + +
+ +
+
+
+ +
+
+
+
+ + + + + + + diff --git a/blade-core-secure/pom.xml b/blade-core-secure/pom.xml new file mode 100644 index 0000000..4c039fc --- /dev/null +++ b/blade-core-secure/pom.xml @@ -0,0 +1,46 @@ + + + 4.0.0 + + BladeX-Tool + org.springblade + ${revision} + + + blade-core-secure + ${project.artifactId} + ${project.parent.version} + jar + + + + + org.springblade + blade-starter-auth + + + org.springblade + blade-starter-cache + + + + org.springframework.boot + spring-boot-starter-jdbc + + + tomcat-jdbc + org.apache.tomcat + + + + + + org.springblade + blade-core-auto + provided + + + + diff --git a/blade-core-secure/src/main/java/org/springblade/core/secure/annotation/PreAuth.java b/blade-core-secure/src/main/java/org/springblade/core/secure/annotation/PreAuth.java new file mode 100644 index 0000000..7b6a294 --- /dev/null +++ b/blade-core-secure/src/main/java/org/springblade/core/secure/annotation/PreAuth.java @@ -0,0 +1,50 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.secure.annotation; + +import java.lang.annotation.*; + +/** + * 权限注解 用于检查权限 规定访问权限 + * + * @example @PreAuth("#userVO.id<10") + * @example @PreAuth("hasRole(#test, #test1)") + * @example @PreAuth("hasPermission(#test) and @PreAuth.hasPermission(#test)") + * @author Chill + */ +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +public @interface PreAuth { + + /** + * Spring el表达式 + */ + String value(); + +} + diff --git a/blade-core-secure/src/main/java/org/springblade/core/secure/aspect/AuthAspect.java b/blade-core-secure/src/main/java/org/springblade/core/secure/aspect/AuthAspect.java new file mode 100644 index 0000000..9ae4aef --- /dev/null +++ b/blade-core-secure/src/main/java/org/springblade/core/secure/aspect/AuthAspect.java @@ -0,0 +1,132 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.secure.aspect; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.reflect.MethodSignature; +import org.springblade.core.secure.annotation.PreAuth; +import org.springblade.core.secure.auth.AuthFun; +import org.springblade.core.secure.exception.SecureException; +import org.springblade.core.tool.api.ResultCode; +import org.springblade.core.tool.utils.ClassUtil; +import org.springblade.core.tool.utils.StringUtil; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.expression.BeanFactoryResolver; +import org.springframework.core.MethodParameter; +import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.lang.NonNull; + +import java.lang.reflect.Method; + +/** + * AOP 鉴权 + * + * @author Chill + */ +@Aspect +public class AuthAspect implements ApplicationContextAware { + + /** + * 表达式处理 + */ + private static final ExpressionParser EXPRESSION_PARSER = new SpelExpressionParser(); + + /** + * 切 方法 和 类上的 @PreAuth 注解 + * + * @param point 切点 + * @return Object + * @throws Throwable 没有权限的异常 + */ + @Around( + "@annotation(org.springblade.core.secure.annotation.PreAuth) || " + + "@within(org.springblade.core.secure.annotation.PreAuth)" + ) + public Object preAuth(ProceedingJoinPoint point) throws Throwable { + if (handleAuth(point)) { + return point.proceed(); + } + throw new SecureException(ResultCode.UN_AUTHORIZED); + } + + /** + * 处理权限 + * + * @param point 切点 + */ + private boolean handleAuth(ProceedingJoinPoint point) { + MethodSignature ms = (MethodSignature) point.getSignature(); + Method method = ms.getMethod(); + // 读取权限注解,优先方法上,没有则读取类 + PreAuth preAuth = ClassUtil.getAnnotation(method, PreAuth.class); + // 判断表达式 + String condition = preAuth.value(); + if (StringUtil.isNotBlank(condition)) { + Expression expression = EXPRESSION_PARSER.parseExpression(condition); + // 方法参数值 + Object[] args = point.getArgs(); + StandardEvaluationContext context = getEvaluationContext(method, args); + return expression.getValue(context, Boolean.class); + } + return false; + } + + /** + * 获取方法上的参数 + * + * @param method 方法 + * @param args 变量 + * @return {SimpleEvaluationContext} + */ + private StandardEvaluationContext getEvaluationContext(Method method, Object[] args) { + // 初始化Sp el表达式上下文,并设置 AuthFun + StandardEvaluationContext context = new StandardEvaluationContext(new AuthFun()); + // 设置表达式支持spring bean + context.setBeanResolver(new BeanFactoryResolver(applicationContext)); + for (int i = 0; i < args.length; i++) { + // 读取方法参数 + MethodParameter methodParam = ClassUtil.getMethodParameter(method, i); + // 设置方法 参数名和值 为sp el变量 + context.setVariable(methodParam.getParameterName(), args[i]); + } + return context; + } + + private ApplicationContext applicationContext; + + @Override + public void setApplicationContext(@NonNull ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + +} diff --git a/blade-core-secure/src/main/java/org/springblade/core/secure/auth/AuthFun.java b/blade-core-secure/src/main/java/org/springblade/core/secure/auth/AuthFun.java new file mode 100644 index 0000000..15e9550 --- /dev/null +++ b/blade-core-secure/src/main/java/org/springblade/core/secure/auth/AuthFun.java @@ -0,0 +1,223 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.secure.auth; + +import org.springblade.core.jwt.JwtUtil; +import org.springblade.core.launch.constant.TokenConstant; +import org.springblade.core.secure.BladeUser; +import org.springblade.core.secure.handler.IPermissionHandler; +import org.springblade.core.secure.utils.AuthUtil; +import org.springblade.core.tool.constant.RoleConstant; +import org.springblade.core.tool.utils.*; + +import jakarta.servlet.http.HttpServletRequest; + +import java.util.Objects; + +/** + * 权限判断 + * + * @author Chill + */ +public class AuthFun { + + /** + * 权限校验处理器 + */ + private static IPermissionHandler permissionHandler; + + private static IPermissionHandler getPermissionHandler() { + if (permissionHandler == null) { + permissionHandler = SpringUtil.getBean(IPermissionHandler.class); + } + return permissionHandler; + } + + /** + * 判断角色是否具有接口权限 + * + * @return {boolean} + */ + public boolean permissionAll() { + return getPermissionHandler().permissionAll(); + } + + /** + * 判断角色是否具有接口权限 + * + * @param permission 权限编号 + * @return {boolean} + */ + public boolean hasPermission(String permission) { + return getPermissionHandler().hasPermission(permission); + } + + /** + * 放行所有请求 + * + * @return {boolean} + */ + public boolean permitAll() { + return true; + } + + /** + * 只有超管角色才可访问 + * + * @return {boolean} + */ + public boolean denyAll() { + return hasRole(RoleConstant.ADMIN); + } + + /** + * 是否已授权 + * + * @return {boolean} + */ + public boolean hasAuth() { + return Func.isNotEmpty(AuthUtil.getUser()); + } + + /** + * 是否有时间授权 + * + * @param start 开始时间 + * @param end 结束时间 + * @return {boolean} + */ + public boolean hasTimeAuth(Integer start, Integer end) { + Integer hour = DateUtil.hour(); + return hour >= start && hour <= end; + } + + /** + * 判断是否有该角色权限 + * + * @param role 单角色 + * @return {boolean} + */ + public boolean hasRole(String role) { + return hasAnyRole(role); + } + + /** + * 判断是否具有所有角色权限 + * + * @param role 角色集合 + * @return {boolean} + */ + public boolean hasAllRole(String... role) { + for (String r : role) { + if (!hasRole(r)) { + return false; + } + } + return true; + } + + /** + * 判断是否有该角色权限 + * + * @param role 角色集合 + * @return {boolean} + */ + public boolean hasAnyRole(String... role) { + BladeUser user = AuthUtil.getUser(); + if (user == null) { + return false; + } + String userRole = user.getRoleName(); + if (StringUtil.isBlank(userRole)) { + return false; + } + String[] roles = Func.toStrArray(userRole); + for (String r : role) { + if (CollectionUtil.contains(roles, r)) { + return true; + } + } + return false; + } + + /** + * 判断请求是否为加密token + * + * @return {boolean} + */ + public boolean hasCrypto() { + HttpServletRequest request = WebUtil.getRequest(); + String auth = Objects.requireNonNull(request).getHeader(TokenConstant.HEADER); + return JwtUtil.isCrypto( + StringUtil.isNotBlank(auth) ? auth : request.getParameter(TokenConstant.HEADER) + ); + } + + /** + * 判断令牌是否符合严格模式 + * + * @return {boolean} + */ + public boolean hasStrictToken() { + BladeUser currentUser = AuthUtil.getUser(); + return AuthUtil.userIncomplete(currentUser); + } + + /** + * 判断是否包含安全请求头 + * + * @return {boolean} + */ + public boolean hasStrictHeader() { + return !AuthUtil.secureHeaderIncomplete(); + } + + /** + * 判断是否有该请求头 + * + * @param header 请求头 + * @return {boolean} + */ + public boolean hasHeader(String header) { + HttpServletRequest request = WebUtil.getRequest(); + String value = Objects.requireNonNull(request).getHeader(header); + return StringUtil.isNotBlank(value); + } + + /** + * 判断是否有该请求头 + * + * @param header 请求头 + * @param key 请求值 + * @return {boolean} + */ + public boolean hasHeader(String header, String key) { + HttpServletRequest request = WebUtil.getRequest(); + String value = Objects.requireNonNull(request).getHeader(header); + return StringUtil.equals(value, key); + } + +} diff --git a/blade-core-secure/src/main/java/org/springblade/core/secure/config/RegistryConfiguration.java b/blade-core-secure/src/main/java/org/springblade/core/secure/config/RegistryConfiguration.java new file mode 100644 index 0000000..6771e15 --- /dev/null +++ b/blade-core-secure/src/main/java/org/springblade/core/secure/config/RegistryConfiguration.java @@ -0,0 +1,71 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.secure.config; + + +import lombok.AllArgsConstructor; +import org.springblade.core.secure.handler.BladePermissionHandler; +import org.springblade.core.secure.handler.IPermissionHandler; +import org.springblade.core.secure.handler.ISecureHandler; +import org.springblade.core.secure.handler.SecureHandlerHandler; +import org.springblade.core.secure.registry.SecureRegistry; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.core.annotation.Order; +import org.springframework.jdbc.core.JdbcTemplate; + +/** + * secure注册默认配置 + * + * @author Chill + */ +@Order +@AutoConfiguration(before = SecureConfiguration.class) +@AllArgsConstructor +public class RegistryConfiguration { + + private final JdbcTemplate jdbcTemplate; + + @Bean + @ConditionalOnMissingBean(SecureRegistry.class) + public SecureRegistry secureRegistry() { + return new SecureRegistry(); + } + + @Bean + @ConditionalOnMissingBean(ISecureHandler.class) + public ISecureHandler secureHandler() { + return new SecureHandlerHandler(); + } + + @Bean + @ConditionalOnMissingBean(IPermissionHandler.class) + public IPermissionHandler permissionHandler() { + return new BladePermissionHandler(jdbcTemplate); + } + +} diff --git a/blade-core-secure/src/main/java/org/springblade/core/secure/config/SecureConfiguration.java b/blade-core-secure/src/main/java/org/springblade/core/secure/config/SecureConfiguration.java new file mode 100644 index 0000000..68fda37 --- /dev/null +++ b/blade-core-secure/src/main/java/org/springblade/core/secure/config/SecureConfiguration.java @@ -0,0 +1,135 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.secure.config; + + +import lombok.AllArgsConstructor; +import org.springblade.core.launch.props.BladeProperties; +import org.springblade.core.secure.aspect.AuthAspect; +import org.springblade.core.secure.handler.ISecureHandler; +import org.springblade.core.secure.props.AuthSecure; +import org.springblade.core.secure.props.BasicSecure; +import org.springblade.core.secure.props.BladeSecureProperties; +import org.springblade.core.secure.props.SignSecure; +import org.springblade.core.secure.registry.SecureRegistry; +import org.springblade.core.tool.utils.StringPool; +import org.springblade.core.tool.utils.StringUtil; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.core.annotation.Order; +import org.springframework.lang.NonNull; +import org.springframework.web.servlet.config.annotation.InterceptorRegistration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * 安全配置类 + * + * @author Chill + */ +@Order +@AutoConfiguration +@AllArgsConstructor +@EnableConfigurationProperties({BladeSecureProperties.class}) +public class SecureConfiguration implements WebMvcConfigurer { + + private final SecureRegistry secureRegistry; + + private final BladeProperties bladeProperties; + + private final BladeSecureProperties secureProperties; + + private final ISecureHandler secureHandler; + + @Override + public void addInterceptors(@NonNull InterceptorRegistry registry) { + // 设置请求授权 + if (secureRegistry.isAuthEnabled() || secureProperties.getAuthEnabled()) { + List authSecures = this.secureRegistry.addAuthPatterns(secureProperties.getAuth()).getAuthSecures(); + if (!authSecures.isEmpty()) { + registry.addInterceptor(secureHandler.authInterceptor(secureProperties, authSecures)); + // 设置路径放行 + secureRegistry.excludePathPatterns(authSecures.stream().map(AuthSecure::getPattern).collect(Collectors.toList())); + } + } + // 设置基础认证授权 + if (secureRegistry.isBasicEnabled() || secureProperties.getBasicEnabled()) { + List basicSecures = this.secureRegistry.addBasicPatterns(secureProperties.getBasic()).getBasicSecures(); + if (!basicSecures.isEmpty()) { + registry.addInterceptor(secureHandler.basicInterceptor(basicSecures)); + // 设置路径放行 + secureRegistry.excludePathPatterns(basicSecures.stream().map(BasicSecure::getPattern).collect(Collectors.toList())); + } + } + // 设置签名认证授权 + if (secureRegistry.isSignEnabled() || secureProperties.getSignEnabled()) { + List signSecures = this.secureRegistry.addSignPatterns(secureProperties.getSign()).getSignSecures(); + if (!signSecures.isEmpty()) { + registry.addInterceptor(secureHandler.signInterceptor(signSecures)); + // 设置路径放行 + secureRegistry.excludePathPatterns(signSecures.stream().map(SignSecure::getPattern).collect(Collectors.toList())); + } + } + // 设置客户端授权 + if (secureRegistry.isClientEnabled() || secureProperties.getClientEnabled()) { + secureProperties.getClient().forEach( + clientSecure -> registry.addInterceptor(secureHandler.clientInterceptor(clientSecure.getClientId())) + .addPathPatterns(clientSecure.getPathPatterns()) + ); + } + // 设置令牌严格模式 + if (!secureRegistry.isStrictToken()) { + secureProperties.setStrictToken(false); + } + // 设置请求头严格模式 + if (!secureRegistry.isStrictHeader()) { + secureProperties.setStrictHeader(false); + } + // 设置路径放行 + if (secureRegistry.isEnabled() || secureProperties.getEnabled()) { + InterceptorRegistration interceptorRegistration = registry.addInterceptor(secureHandler.tokenInterceptor(secureProperties)) + .excludePathPatterns(secureRegistry.getExcludePatterns()) + .excludePathPatterns(secureRegistry.getDefaultExcludePatterns()) + .excludePathPatterns(secureProperties.getSkipUrl()); + // 宽松模式下获取放行路径且再新建一套自定义放行路径,用于处理cloud网关虚拟路径导致未匹配的问题 + // 严格模式下不予处理,应严格按照cloud和boot的路由进行匹配 + if (!secureProperties.getStrictToken()) { + interceptorRegistration.excludePathPatterns(secureProperties.getSkipUrl().stream() + .map(url -> StringUtil.removePrefix(url, StringPool.SLASH + bladeProperties.getName())).toList()); + } + } + } + + @Bean + public AuthAspect authAspect() { + return new AuthAspect(); + } + +} diff --git a/blade-core-secure/src/main/java/org/springblade/core/secure/constant/AuthConstant.java b/blade-core-secure/src/main/java/org/springblade/core/secure/constant/AuthConstant.java new file mode 100644 index 0000000..1e443be --- /dev/null +++ b/blade-core-secure/src/main/java/org/springblade/core/secure/constant/AuthConstant.java @@ -0,0 +1,95 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.secure.constant; + +/** + * PreAuth权限表达式 + * + * @author Chill + */ +public interface AuthConstant { + + /** + * 超管别名 + */ + String ADMINISTRATOR = "administrator"; + + /** + * 是有超管角色 + */ + String HAS_ROLE_ADMINISTRATOR = "hasRole('" + ADMINISTRATOR + "')"; + + /** + * 管理员别名 + */ + String ADMIN = "admin"; + + /** + * 是否有管理员角色 + */ + String HAS_ROLE_ADMIN = "hasAnyRole('" + ADMINISTRATOR + "', '" + ADMIN + "')"; + + /** + * 用户别名 + */ + String USER = "user"; + + /** + * 是否有用户角色 + */ + String HAS_ROLE_USER = "hasRole('" + USER + "')"; + + /** + * 测试别名 + */ + String TEST = "test"; + + /** + * 是否有测试角色 + */ + String HAS_ROLE_TEST = "hasRole('" + TEST + "')"; + + /** + * 放行所有请求 + */ + String PERMIT_ALL = "permitAll()"; + + /** + * 只有超管才能访问 + */ + String DENY_ALL = "denyAll()"; + + /** + * 对所有请求进行接口权限校验 + */ + String PERMISSION_ALL = "permissionAll()"; + + /** + * 是否对token加密传输 + */ + String HAS_CRYPTO = "hasCrypto()"; + +} diff --git a/blade-core-secure/src/main/java/org/springblade/core/secure/constant/PermissionConstant.java b/blade-core-secure/src/main/java/org/springblade/core/secure/constant/PermissionConstant.java new file mode 100644 index 0000000..b04f521 --- /dev/null +++ b/blade-core-secure/src/main/java/org/springblade/core/secure/constant/PermissionConstant.java @@ -0,0 +1,71 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.secure.constant; + +import org.springblade.core.tool.utils.StringUtil; + +/** + * 权限校验常量 + * + * @author Chill + */ +public interface PermissionConstant { + + /** + * 获取角色所有的权限编号 + * + * @param size 数量 + * @return string + */ + static String permissionAllStatement(int size) { + return "select scope_path as path from blade_scope_api where id in (select scope_id from blade_role_scope where scope_category = 2 and role_id in (" + buildHolder(size) + "))"; + } + + /** + * 获取角色指定的权限编号 + * + * @param size 数量 + * @return string + */ + static String permissionStatement(int size) { + return "select resource_code as code from blade_scope_api where resource_code = ? and id in (select scope_id from blade_role_scope where scope_category = 2 and role_id in (" + buildHolder(size) + "))"; + } + + /** + * 获取Sql占位符 + * + * @param size 数量 + * @return String + */ + static String buildHolder(int size) { + StringBuilder builder = StringUtil.builder(); + for (int i = 0; i < size; i++) { + builder.append("?,"); + } + return StringUtil.removeSuffix(builder.toString(), ","); + } + +} diff --git a/blade-core-secure/src/main/java/org/springblade/core/secure/constant/SecureConstant.java b/blade-core-secure/src/main/java/org/springblade/core/secure/constant/SecureConstant.java new file mode 100644 index 0000000..3376ead --- /dev/null +++ b/blade-core-secure/src/main/java/org/springblade/core/secure/constant/SecureConstant.java @@ -0,0 +1,91 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.secure.constant; + +/** + * 授权校验常量 + * + * @author Chill + */ +public interface SecureConstant { + + /** + * 认证请求头 + */ + String BASIC_HEADER_KEY = "Authorization"; + + /** + * 认证请求头前缀 + */ + String BASIC_HEADER_PREFIX = "Basic "; + + /** + * 认证请求头前缀 + */ + String BASIC_HEADER_PREFIX_EXT = "Basic%20"; + + /** + * 认证请求头 + */ + String BASIC_REALM_HEADER_KEY = "WWW-Authenticate"; + + /** + * 认证请求值 + */ + String BASIC_REALM_HEADER_VALUE = "basic realm=\"no auth\""; + + /** + * 授权认证失败 + */ + String AUTHORIZATION_FAILED = "授权认证失败"; + + /** + * 签名认证失败 + */ + String SIGN_FAILED = "签名认证失败"; + + /** + * 用户信息不完整 + */ + String USER_INCOMPLETE = "用户信息不完整,签名认证失败"; + /** + * 请求头信息不完整 + */ + String SECURE_HEADER_INCOMPLETE = "请求头信息不完整,签名认证失败"; + /** + * 客户端令牌解析失败 + */ + String CLIENT_TOKEN_PARSE_FAILED = "客户端令牌解析失败"; + /** + * 客户端令牌不合法 + */ + String INVALID_CLIENT_TOKEN = "客户端令牌不合法"; + /** + * Authorization未找到 + */ + String AUTHORIZATION_NOT_FOUND = "请求头中未找到 [Authorization] 信息"; + +} diff --git a/blade-core-secure/src/main/java/org/springblade/core/secure/handler/BladePermissionHandler.java b/blade-core-secure/src/main/java/org/springblade/core/secure/handler/BladePermissionHandler.java new file mode 100644 index 0000000..d3b4e6c --- /dev/null +++ b/blade-core-secure/src/main/java/org/springblade/core/secure/handler/BladePermissionHandler.java @@ -0,0 +1,119 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.secure.handler; + +import lombok.AllArgsConstructor; +import org.springblade.core.cache.utils.CacheUtil; +import org.springblade.core.secure.BladeUser; +import org.springblade.core.secure.utils.AuthUtil; +import org.springblade.core.tool.utils.Func; +import org.springblade.core.tool.utils.StringPool; +import org.springblade.core.tool.utils.WebUtil; +import org.springframework.jdbc.core.JdbcTemplate; + +import jakarta.servlet.http.HttpServletRequest; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.springblade.core.cache.constant.CacheConstant.SYS_CACHE; +import static org.springblade.core.secure.constant.PermissionConstant.permissionAllStatement; +import static org.springblade.core.secure.constant.PermissionConstant.permissionStatement; + +/** + * 默认授权校验类 + * + * @author Chill + */ +@AllArgsConstructor +public class BladePermissionHandler implements IPermissionHandler { + + private static final String SCOPE_CACHE_CODE = "apiScope:code:"; + + private final JdbcTemplate jdbcTemplate; + + @Override + public boolean permissionAll() { + HttpServletRequest request = WebUtil.getRequest(); + BladeUser user = AuthUtil.getUser(); + if (request == null || user == null) { + return false; + } + String uri = request.getRequestURI(); + List paths = permissionPath(user.getRoleId()); + if (paths.size() == 0) { + return false; + } + return paths.stream().anyMatch(uri::contains); + } + + @Override + public boolean hasPermission(String permission) { + HttpServletRequest request = WebUtil.getRequest(); + BladeUser user = AuthUtil.getUser(); + if (request == null || user == null) { + return false; + } + List codes = permissionCode(permission, user.getRoleId()); + return codes.size() != 0; + } + + /** + * 获取接口权限地址 + * + * @param roleId 角色id + * @return permissions + */ + private List permissionPath(String roleId) { + List permissions = CacheUtil.get(SYS_CACHE, SCOPE_CACHE_CODE, roleId, List.class, Boolean.FALSE); + if (permissions == null) { + List roleIds = Func.toLongList(roleId); + permissions = jdbcTemplate.queryForList(permissionAllStatement(roleIds.size()), roleIds.toArray(), String.class); + CacheUtil.put(SYS_CACHE, SCOPE_CACHE_CODE, roleId, permissions, Boolean.FALSE); + } + return permissions; + } + + /** + * 获取接口权限信息 + * + * @param permission 权限编号 + * @param roleId 角色id + * @return permissions + */ + private List permissionCode(String permission, String roleId) { + List permissions = CacheUtil.get(SYS_CACHE, SCOPE_CACHE_CODE, permission + StringPool.COLON + roleId, List.class, Boolean.FALSE); + if (permissions == null) { + List args = new ArrayList<>(Collections.singletonList(permission)); + List roleIds = Func.toLongList(roleId); + args.addAll(roleIds); + permissions = jdbcTemplate.queryForList(permissionStatement(roleIds.size()), args.toArray(), String.class); + CacheUtil.put(SYS_CACHE, SCOPE_CACHE_CODE, permission + StringPool.COLON + roleId, permissions, Boolean.FALSE); + } + return permissions; + } + +} diff --git a/blade-core-secure/src/main/java/org/springblade/core/secure/handler/IPermissionHandler.java b/blade-core-secure/src/main/java/org/springblade/core/secure/handler/IPermissionHandler.java new file mode 100644 index 0000000..491a712 --- /dev/null +++ b/blade-core-secure/src/main/java/org/springblade/core/secure/handler/IPermissionHandler.java @@ -0,0 +1,50 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.secure.handler; + +/** + * 权限校验通用接口 + * + * @author Chill + */ +public interface IPermissionHandler { + + /** + * 判断角色是否具有接口权限 + * + * @return {boolean} + */ + boolean permissionAll(); + + /** + * 判断角色是否具有接口权限 + * + * @param permission 权限编号 + * @return {boolean} + */ + boolean hasPermission(String permission); + +} diff --git a/blade-core-secure/src/main/java/org/springblade/core/secure/handler/ISecureHandler.java b/blade-core-secure/src/main/java/org/springblade/core/secure/handler/ISecureHandler.java new file mode 100644 index 0000000..66b8451 --- /dev/null +++ b/blade-core-secure/src/main/java/org/springblade/core/secure/handler/ISecureHandler.java @@ -0,0 +1,83 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.secure.handler; + +import org.springblade.core.secure.props.AuthSecure; +import org.springblade.core.secure.props.BasicSecure; +import org.springblade.core.secure.props.BladeSecureProperties; +import org.springblade.core.secure.props.SignSecure; +import org.springframework.web.servlet.HandlerInterceptor; + +import java.util.List; + +/** + * secure 拦截器集合 + * + * @author Chill + */ +public interface ISecureHandler { + + /** + * token拦截器 + * + * @param secureProperties 授权配置 + * @return tokenInterceptor + */ + HandlerInterceptor tokenInterceptor(BladeSecureProperties secureProperties); + + /** + * auth拦截器 + * + * @param authSecures 授权集合 + * @return HandlerInterceptor + */ + HandlerInterceptor authInterceptor(BladeSecureProperties secureProperties, List authSecures); + + /** + * basic拦截器 + * + * @param basicSecures 基础认证集合 + * @return HandlerInterceptor + */ + HandlerInterceptor basicInterceptor(List basicSecures); + + /** + * sign拦截器 + * + * @param signSecures 签名认证集合 + * @return HandlerInterceptor + */ + HandlerInterceptor signInterceptor(List signSecures); + + /** + * client拦截器 + * + * @param clientId 客户端id + * @return clientInterceptor + */ + HandlerInterceptor clientInterceptor(String clientId); + +} diff --git a/blade-core-secure/src/main/java/org/springblade/core/secure/handler/SecureHandlerHandler.java b/blade-core-secure/src/main/java/org/springblade/core/secure/handler/SecureHandlerHandler.java new file mode 100644 index 0000000..a32d4b5 --- /dev/null +++ b/blade-core-secure/src/main/java/org/springblade/core/secure/handler/SecureHandlerHandler.java @@ -0,0 +1,69 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.secure.handler; + +import org.springblade.core.secure.interceptor.*; +import org.springblade.core.secure.props.AuthSecure; +import org.springblade.core.secure.props.BasicSecure; +import org.springblade.core.secure.props.BladeSecureProperties; +import org.springblade.core.secure.props.SignSecure; +import org.springframework.web.servlet.HandlerInterceptor; + +import java.util.List; + +/** + * Secure处理器 + * + * @author Chill + */ +public class SecureHandlerHandler implements ISecureHandler { + + @Override + public HandlerInterceptor tokenInterceptor(BladeSecureProperties secureProperties) { + return new TokenInterceptor(secureProperties); + } + + @Override + public HandlerInterceptor authInterceptor(BladeSecureProperties secureProperties, List authSecures) { + return new AuthInterceptor(authSecures); + } + + @Override + public HandlerInterceptor basicInterceptor(List basicSecures) { + return new BasicInterceptor(basicSecures); + } + + @Override + public HandlerInterceptor signInterceptor(List signSecures) { + return new SignInterceptor(signSecures); + } + + @Override + public HandlerInterceptor clientInterceptor(String clientId) { + return new ClientInterceptor(clientId); + } + +} diff --git a/blade-core-secure/src/main/java/org/springblade/core/secure/interceptor/AuthInterceptor.java b/blade-core-secure/src/main/java/org/springblade/core/secure/interceptor/AuthInterceptor.java new file mode 100644 index 0000000..7197067 --- /dev/null +++ b/blade-core-secure/src/main/java/org/springblade/core/secure/interceptor/AuthInterceptor.java @@ -0,0 +1,117 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.secure.interceptor; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.secure.auth.AuthFun; +import org.springblade.core.secure.props.AuthSecure; +import org.springblade.core.secure.provider.HttpMethod; +import org.springblade.core.secure.provider.ResponseProvider; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.lang.NonNull; +import org.springframework.util.AntPathMatcher; +import org.springframework.web.servlet.HandlerInterceptor; + +import java.util.List; + +import static org.springblade.core.secure.constant.SecureConstant.AUTHORIZATION_FAILED; + +/** + * 自定义授权拦截器校验 + * + * @author Chill + */ +@Slf4j +@AllArgsConstructor +public class AuthInterceptor implements HandlerInterceptor { + + /** + * 表达式处理 + */ + private static final ExpressionParser EXPRESSION_PARSER = new SpelExpressionParser(); + private static final EvaluationContext EVALUATION_CONTEXT = new StandardEvaluationContext(new AuthFun()); + private static final AntPathMatcher ANT_PATH_MATCHER = new AntPathMatcher(); + + /** + * 授权集合 + */ + private final List authSecures; + + @Override + public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) { + boolean check = authSecures.stream().filter(authSecure -> checkAuth(request, authSecure)).findFirst().map( + authSecure -> checkExpression(authSecure.getExpression()) + ).orElse(Boolean.TRUE); + if (!check) { + ResponseProvider.logAuthFailure(request, response, AUTHORIZATION_FAILED); + return false; + } + return true; + } + + /** + * 检测授权 + */ + private boolean checkAuth(HttpServletRequest request, AuthSecure authSecure) { + return checkMethod(request, authSecure.getMethod()) && checkPath(request, authSecure.getPattern()); + } + + /** + * 检测请求方法 + */ + private boolean checkMethod(HttpServletRequest request, HttpMethod method) { + return method == HttpMethod.ALL || ( + method != null && method == HttpMethod.of(request.getMethod()) + ); + } + + /** + * 检测路径匹配 + */ + private boolean checkPath(HttpServletRequest request, String pattern) { + String servletPath = request.getServletPath(); + String pathInfo = request.getPathInfo(); + if (pathInfo != null && !pathInfo.isEmpty()) { + servletPath = servletPath + pathInfo; + } + return ANT_PATH_MATCHER.match(pattern, servletPath); + } + + /** + * 检测表达式 + */ + private boolean checkExpression(String expression) { + Boolean result = EXPRESSION_PARSER.parseExpression(expression).getValue(EVALUATION_CONTEXT, Boolean.class); + return result != null ? result : false; + } + +} diff --git a/blade-core-secure/src/main/java/org/springblade/core/secure/interceptor/BasicInterceptor.java b/blade-core-secure/src/main/java/org/springblade/core/secure/interceptor/BasicInterceptor.java new file mode 100644 index 0000000..2c4562c --- /dev/null +++ b/blade-core-secure/src/main/java/org/springblade/core/secure/interceptor/BasicInterceptor.java @@ -0,0 +1,122 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.secure.interceptor; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.secure.props.BasicSecure; +import org.springblade.core.secure.provider.HttpMethod; +import org.springblade.core.secure.provider.ResponseProvider; +import org.springblade.core.secure.utils.SecureUtil; +import org.springblade.core.tool.jackson.JsonUtil; +import org.springblade.core.tool.utils.WebUtil; +import org.springframework.lang.NonNull; +import org.springframework.util.AntPathMatcher; +import org.springframework.web.servlet.HandlerInterceptor; + +import java.util.List; + +import static org.springblade.core.secure.constant.SecureConstant.BASIC_REALM_HEADER_KEY; +import static org.springblade.core.secure.constant.SecureConstant.BASIC_REALM_HEADER_VALUE; + +/** + * 基础认证拦截器校验 + * + * @author Chill + */ +@Slf4j +@AllArgsConstructor +public class BasicInterceptor implements HandlerInterceptor { + + /** + * 表达式匹配 + */ + private static final AntPathMatcher ANT_PATH_MATCHER = new AntPathMatcher(); + + /** + * 授权集合 + */ + private final List basicSecures; + + @Override + public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) { + boolean check = basicSecures.stream().filter(basicSecure -> checkAuth(request, basicSecure)).findFirst().map( + authSecure -> checkBasic(authSecure.getUsername(), authSecure.getPassword()) + ).orElse(Boolean.TRUE); + if (!check) { + log.warn("授权认证失败,请求接口:{},请求IP:{},请求参数:{}", request.getRequestURI(), WebUtil.getIP(request), JsonUtil.toJson(request.getParameterMap())); + response.setHeader(BASIC_REALM_HEADER_KEY, BASIC_REALM_HEADER_VALUE); + ResponseProvider.write(response); + return false; + } + return true; + } + + /** + * 检测授权 + */ + private boolean checkAuth(HttpServletRequest request, BasicSecure basicSecure) { + return checkMethod(request, basicSecure.getMethod()) && checkPath(request, basicSecure.getPattern()); + } + + /** + * 检测请求方法 + */ + private boolean checkMethod(HttpServletRequest request, HttpMethod method) { + return method == HttpMethod.ALL || ( + method != null && method == HttpMethod.of(request.getMethod()) + ); + } + + /** + * 检测路径匹配 + */ + private boolean checkPath(HttpServletRequest request, String pattern) { + String servletPath = request.getServletPath(); + String pathInfo = request.getPathInfo(); + if (pathInfo != null && !pathInfo.isEmpty()) { + servletPath = servletPath + pathInfo; + } + return ANT_PATH_MATCHER.match(pattern, servletPath); + } + + /** + * 检测表达式 + */ + private boolean checkBasic(String username, String password) { + try { + String[] tokens = SecureUtil.extractAndDecodeAuthorization(); + return username.equals(tokens[0]) && password.equals(tokens[1]); + } catch (Exception e) { + log.warn("授权认证失败,错误信息:{}", e.getMessage()); + return false; + } + } + + +} diff --git a/blade-core-secure/src/main/java/org/springblade/core/secure/interceptor/ClientInterceptor.java b/blade-core-secure/src/main/java/org/springblade/core/secure/interceptor/ClientInterceptor.java new file mode 100644 index 0000000..3eb7326 --- /dev/null +++ b/blade-core-secure/src/main/java/org/springblade/core/secure/interceptor/ClientInterceptor.java @@ -0,0 +1,69 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.secure.interceptor; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.secure.BladeUser; +import org.springblade.core.secure.provider.ResponseProvider; +import org.springblade.core.secure.utils.AuthUtil; +import org.springblade.core.secure.utils.SecureUtil; +import org.springblade.core.tool.jackson.JsonUtil; +import org.springblade.core.tool.utils.StringUtil; +import org.springblade.core.tool.utils.WebUtil; +import org.springframework.lang.NonNull; +import org.springframework.web.servlet.HandlerInterceptor; + +/** + * 客户端校验拦截器 + * + * @author Chill + */ +@Slf4j +@AllArgsConstructor +public class ClientInterceptor implements HandlerInterceptor { + + private final String clientId; + + @Override + public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) { + BladeUser user = AuthUtil.getUser(); + boolean check = ( + user != null && + StringUtil.equals(clientId, SecureUtil.getClientId()) && + StringUtil.equals(clientId, user.getClientId()) + ); + if (!check) { + log.warn("客户端认证失败,请求接口:{},请求IP:{},请求参数:{}", request.getRequestURI(), WebUtil.getIP(request), JsonUtil.toJson(request.getParameterMap())); + ResponseProvider.write(response); + return false; + } + return true; + } + +} diff --git a/blade-core-secure/src/main/java/org/springblade/core/secure/interceptor/SignInterceptor.java b/blade-core-secure/src/main/java/org/springblade/core/secure/interceptor/SignInterceptor.java new file mode 100644 index 0000000..98684ad --- /dev/null +++ b/blade-core-secure/src/main/java/org/springblade/core/secure/interceptor/SignInterceptor.java @@ -0,0 +1,179 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.secure.interceptor; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.secure.props.SignSecure; +import org.springblade.core.secure.provider.HttpMethod; +import org.springblade.core.secure.provider.ResponseProvider; +import org.springblade.core.tool.jackson.JsonUtil; +import org.springblade.core.tool.utils.DateUtil; +import org.springblade.core.tool.utils.DigestUtil; +import org.springblade.core.tool.utils.Func; +import org.springblade.core.tool.utils.WebUtil; +import org.springframework.lang.NonNull; +import org.springframework.util.AntPathMatcher; +import org.springframework.web.servlet.HandlerInterceptor; + +import java.time.Duration; +import java.util.Date; +import java.util.List; + +/** + * 签名认证拦截器校验 + * + * @author Chill + */ +@Slf4j +@AllArgsConstructor +public class SignInterceptor implements HandlerInterceptor { + + /** + * 表达式匹配 + */ + private static final AntPathMatcher ANT_PATH_MATCHER = new AntPathMatcher(); + + /** + * 授权集合 + */ + private final List signSecures; + + /** + * 请求时间 + */ + private final static String TIMESTAMP = "timestamp"; + + /** + * 随机数 + */ + private final static String NONCE = "nonce"; + + /** + * 时间随机数组合加密串 + */ + private final static String SIGNATURE = "signature"; + + /** + * sha1加密方式 + */ + private final static String SHA1 = "sha1"; + + /** + * md5加密方式 + */ + private final static String MD5 = "md5"; + + /** + * 时间差最小值 + */ + private final static Integer SECOND_MIN = 0; + + /** + * 时间差最大值 + */ + private final static Integer SECOND_MAX = 10; + + @Override + public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) { + boolean check = signSecures.stream().filter(signSecure -> checkAuth(request, signSecure)).findFirst().map( + authSecure -> checkSign(authSecure.getCrypto()) + ).orElse(Boolean.TRUE); + if (!check) { + log.warn("授权认证失败,请求接口:{},请求IP:{},请求参数:{}", request.getRequestURI(), WebUtil.getIP(request), JsonUtil.toJson(request.getParameterMap())); + ResponseProvider.write(response); + return false; + } + return true; + } + + /** + * 检测授权 + */ + private boolean checkAuth(HttpServletRequest request, SignSecure signSecure) { + return checkMethod(request, signSecure.getMethod()) && checkPath(request, signSecure.getPattern()); + } + + /** + * 检测请求方法 + */ + private boolean checkMethod(HttpServletRequest request, HttpMethod method) { + return method == HttpMethod.ALL || ( + method != null && method == HttpMethod.of(request.getMethod()) + ); + } + + /** + * 检测路径匹配 + */ + private boolean checkPath(HttpServletRequest request, String pattern) { + String servletPath = request.getServletPath(); + String pathInfo = request.getPathInfo(); + if (pathInfo != null && pathInfo.length() > 0) { + servletPath = servletPath + pathInfo; + } + return ANT_PATH_MATCHER.match(pattern, servletPath); + } + + /** + * 检测表达式 + */ + private boolean checkSign(String crypto) { + try { + HttpServletRequest request = WebUtil.getRequest(); + if (request == null) { + return false; + } + // 获取头部动态签名信息 + String timestamp = request.getHeader(TIMESTAMP); + // 判断是否在合法时间段 + long seconds = Duration.between(new Date(Func.toLong(timestamp)).toInstant(), DateUtil.now().toInstant()).getSeconds(); + if (seconds < SECOND_MIN || seconds > SECOND_MAX) { + log.warn("授权认证失败,错误信息:{}", "请求时间戳非法"); + return false; + } + String nonce = request.getHeader(NONCE); + String signature = request.getHeader(SIGNATURE); + // 加密签名比对,可自行拓展加密规则 + String sign; + if (crypto.equals(MD5)) { + sign = DigestUtil.md5Hex(timestamp + nonce); + } else if (crypto.equals(SHA1)) { + sign = DigestUtil.sha1Hex(timestamp + nonce); + } else { + sign = DigestUtil.sha1Hex(timestamp + nonce); + } + return sign.equalsIgnoreCase(signature); + } catch (Exception e) { + log.warn("授权认证失败,错误信息:{}", e.getMessage()); + return false; + } + } + + +} diff --git a/blade-core-secure/src/main/java/org/springblade/core/secure/interceptor/TokenInterceptor.java b/blade-core-secure/src/main/java/org/springblade/core/secure/interceptor/TokenInterceptor.java new file mode 100644 index 0000000..dc09dad --- /dev/null +++ b/blade-core-secure/src/main/java/org/springblade/core/secure/interceptor/TokenInterceptor.java @@ -0,0 +1,98 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.secure.interceptor; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.secure.BladeUser; +import org.springblade.core.secure.props.BladeSecureProperties; +import org.springblade.core.secure.provider.ResponseProvider; +import org.springblade.core.secure.utils.AuthUtil; +import org.springframework.lang.NonNull; +import org.springframework.web.servlet.HandlerInterceptor; + +import java.util.ArrayList; +import java.util.List; + +import static org.springblade.core.secure.constant.SecureConstant.*; + + +/** + * 签名认证拦截器 + * + * @author Chill + */ +@Slf4j +@AllArgsConstructor +public class TokenInterceptor implements HandlerInterceptor { + + private final BladeSecureProperties secureProperties; + private final static List DEFAULT_STRICT_SKIP_URL = new ArrayList<>(); + + static { + DEFAULT_STRICT_SKIP_URL.add("/menu/routes"); + DEFAULT_STRICT_SKIP_URL.add("/menu/buttons"); + DEFAULT_STRICT_SKIP_URL.add("/menu/top-menu"); + DEFAULT_STRICT_SKIP_URL.add("/blade-system/menu/routes"); + DEFAULT_STRICT_SKIP_URL.add("/blade-system/menu/buttons"); + DEFAULT_STRICT_SKIP_URL.add("/blade-system/menu/top-menu"); + } + + @Override + public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) { + BladeUser currentUser = AuthUtil.getUser(); + if (currentUser == null) { + ResponseProvider.logAuthFailure(request, response, SIGN_FAILED); + return false; + } + if (checkStrictToken(request, currentUser)) { + ResponseProvider.logAuthFailure(request, response, USER_INCOMPLETE); + return false; + } + if (checkStrictHeader()) { + ResponseProvider.logAuthFailure(request, response, SECURE_HEADER_INCOMPLETE); + return false; + } + return true; + } + + private boolean checkStrictToken(HttpServletRequest request, BladeUser currentUser) { + String requestUrl = request.getRequestURI(); // 获取当前请求的URL + boolean skip = DEFAULT_STRICT_SKIP_URL.stream() + .anyMatch(requestUrl::equals); // 判断当前请求的URL是否在跳过列表中 + + // 如果请求的URL需要跳过检查或者不需要严格检查Token,则返回false + return !skip && secureProperties.getStrictToken() && AuthUtil.userIncomplete(currentUser); + } + + private boolean checkStrictHeader() { + return secureProperties.getStrictHeader() && AuthUtil.secureHeaderIncomplete(); + } + + +} diff --git a/blade-core-secure/src/main/java/org/springblade/core/secure/props/AuthSecure.java b/blade-core-secure/src/main/java/org/springblade/core/secure/props/AuthSecure.java new file mode 100644 index 0000000..9552d8c --- /dev/null +++ b/blade-core-secure/src/main/java/org/springblade/core/secure/props/AuthSecure.java @@ -0,0 +1,54 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.secure.props; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springblade.core.secure.provider.HttpMethod; + +/** + * 自定义授权规则 + * + * @author Chill + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class AuthSecure { + /** + * 请求方法 + */ + private HttpMethod method; + /** + * 请求路径 + */ + private String pattern; + /** + * 规则表达式 + */ + private String expression; +} diff --git a/blade-core-secure/src/main/java/org/springblade/core/secure/props/BasicSecure.java b/blade-core-secure/src/main/java/org/springblade/core/secure/props/BasicSecure.java new file mode 100644 index 0000000..bdf630a --- /dev/null +++ b/blade-core-secure/src/main/java/org/springblade/core/secure/props/BasicSecure.java @@ -0,0 +1,60 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.secure.props; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springblade.core.secure.provider.HttpMethod; + +/** + * 基础授权规则 + * + * @author Chill + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class BasicSecure { + + /** + * 请求方法 + */ + private HttpMethod method; + /** + * 请求路径 + */ + private String pattern; + /** + * 客户端id + */ + private String username; + /** + * 客户端密钥 + */ + private String password; + +} diff --git a/blade-core-secure/src/main/java/org/springblade/core/secure/props/BladeSecureProperties.java b/blade-core-secure/src/main/java/org/springblade/core/secure/props/BladeSecureProperties.java new file mode 100644 index 0000000..03bd59f --- /dev/null +++ b/blade-core-secure/src/main/java/org/springblade/core/secure/props/BladeSecureProperties.java @@ -0,0 +1,103 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.secure.props; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.ArrayList; +import java.util.List; + +/** + * 客户端校验配置 + * + * @author Chill + */ +@Data +@ConfigurationProperties("blade.secure") +public class BladeSecureProperties { + + /** + * 开启鉴权规则 + */ + private Boolean enabled = false; + + /** + * 开启令牌严格模式 + */ + private Boolean strictToken = true; + + /** + * 开启请求头严格模式 + */ + private Boolean strictHeader = true; + + /** + * 鉴权放行请求 + */ + private final List skipUrl = new ArrayList<>(); + + /** + * 开启授权规则 + */ + private Boolean authEnabled = true; + + /** + * 授权配置 + */ + private final List auth = new ArrayList<>(); + + /** + * 开启基础认证规则 + */ + private Boolean basicEnabled = true; + + /** + * 基础认证配置 + */ + private final List basic = new ArrayList<>(); + + /** + * 开启签名认证规则 + */ + private Boolean signEnabled = true; + + /** + * 签名认证配置 + */ + private final List sign = new ArrayList<>(); + + /** + * 开启客户端规则 + */ + private Boolean clientEnabled = true; + + /** + * 客户端配置 + */ + private final List client = new ArrayList<>(); + +} diff --git a/blade-core-secure/src/main/java/org/springblade/core/secure/props/ClientSecure.java b/blade-core-secure/src/main/java/org/springblade/core/secure/props/ClientSecure.java new file mode 100644 index 0000000..f742304 --- /dev/null +++ b/blade-core-secure/src/main/java/org/springblade/core/secure/props/ClientSecure.java @@ -0,0 +1,50 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.secure.props; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * 客户端令牌认证信息 + * + * @author Chill + */ +@Data +public class ClientSecure { + + /** + * 客户端ID + */ + private String clientId; + /** + * 路径匹配 + */ + private final List pathPatterns = new ArrayList<>(); + +} diff --git a/blade-core-secure/src/main/java/org/springblade/core/secure/props/SignSecure.java b/blade-core-secure/src/main/java/org/springblade/core/secure/props/SignSecure.java new file mode 100644 index 0000000..11225da --- /dev/null +++ b/blade-core-secure/src/main/java/org/springblade/core/secure/props/SignSecure.java @@ -0,0 +1,56 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.secure.props; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springblade.core.secure.provider.HttpMethod; + +/** + * 签名授权规则 + * + * @author Chill + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class SignSecure { + + /** + * 请求方法 + */ + private HttpMethod method; + /** + * 请求路径 + */ + private String pattern; + /** + * 加密方式 + */ + private String crypto; + +} diff --git a/blade-core-secure/src/main/java/org/springblade/core/secure/provider/HttpMethod.java b/blade-core-secure/src/main/java/org/springblade/core/secure/provider/HttpMethod.java new file mode 100644 index 0000000..128c52d --- /dev/null +++ b/blade-core-secure/src/main/java/org/springblade/core/secure/provider/HttpMethod.java @@ -0,0 +1,51 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.secure.provider; + +/** + * HttpMethod枚举类 + * + * @author Chill + */ +public enum HttpMethod { + + /** + * 请求方法集合 + */ + GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE, ALL; + + /** + * 匹配枚举 + */ + public static HttpMethod of(String method) { + try { + return valueOf(method); + } catch (Exception exception) { + return null; + } + } + +} diff --git a/blade-core-secure/src/main/java/org/springblade/core/secure/provider/ResponseProvider.java b/blade-core-secure/src/main/java/org/springblade/core/secure/provider/ResponseProvider.java new file mode 100644 index 0000000..b60251a --- /dev/null +++ b/blade-core-secure/src/main/java/org/springblade/core/secure/provider/ResponseProvider.java @@ -0,0 +1,74 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.secure.provider; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.tool.api.R; +import org.springblade.core.tool.api.ResultCode; +import org.springblade.core.tool.constant.BladeConstant; +import org.springblade.core.tool.jackson.JsonUtil; +import org.springblade.core.tool.utils.WebUtil; +import org.springframework.http.MediaType; + +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.Map; +import java.util.Objects; + +/** + * ResponseProvider + * + * @author Chill + */ +@Slf4j +public class ResponseProvider { + + public static void logAuthFailure(HttpServletRequest request, HttpServletResponse response, String reason) { + try { + Map parameterMap = request.getParameterMap(); + String paramsJson = JsonUtil.toJson(parameterMap); + log.warn("{},请求接口:{},请求IP:{},请求参数:{}", reason, request.getRequestURI(), WebUtil.getIP(request), paramsJson); + } catch (Exception e) { + log.error("日志记录失败", e); + } + ResponseProvider.write(response); + } + + public static void write(HttpServletResponse response) { + R result = R.fail(ResultCode.UN_AUTHORIZED); + response.setCharacterEncoding(BladeConstant.UTF_8); + response.addHeader(BladeConstant.CONTENT_TYPE_NAME, MediaType.APPLICATION_JSON_VALUE); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + try { + response.getWriter().write(Objects.requireNonNull(JsonUtil.toJson(result))); + } catch (IOException ex) { + log.error(ex.getMessage()); + } + } + +} diff --git a/blade-core-secure/src/main/java/org/springblade/core/secure/registry/SecureRegistry.java b/blade-core-secure/src/main/java/org/springblade/core/secure/registry/SecureRegistry.java new file mode 100644 index 0000000..3da6db7 --- /dev/null +++ b/blade-core-secure/src/main/java/org/springblade/core/secure/registry/SecureRegistry.java @@ -0,0 +1,213 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.secure.registry; + +import lombok.Data; +import lombok.Getter; +import org.springblade.core.secure.props.AuthSecure; +import org.springblade.core.secure.props.BasicSecure; +import org.springblade.core.secure.props.SignSecure; +import org.springblade.core.secure.provider.HttpMethod; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * 安全框架统一配置 + * + * @author Chill + */ +@Data +public class SecureRegistry { + + /** + * 是否开启鉴权 + */ + private boolean enabled = true; + + /** + * 开启令牌严格模式 + */ + private boolean strictToken = true; + + /** + * 开启请求头严格模式 + */ + private boolean strictHeader = true; + + /** + * 是否开启授权 + */ + private boolean authEnabled = true; + + /** + * 是否开启基础认证 + */ + private boolean basicEnabled = true; + + /** + * 是否开启签名认证 + */ + private boolean signEnabled = true; + + /** + * 是否开启客户端认证 + */ + private boolean clientEnabled = true; + + /** + * 默认放行规则 + */ + private final List defaultExcludePatterns = new ArrayList<>(); + + /** + * 自定义放行规则 + */ + private final List excludePatterns = new ArrayList<>(); + + /** + * 自定义授权集合 + */ + @Getter + private final List authSecures = new ArrayList<>(); + + /** + * 基础认证集合 + */ + @Getter + private final List basicSecures = new ArrayList<>(); + + /** + * 签名认证集合 + */ + @Getter + private final List signSecures = new ArrayList<>(); + + public SecureRegistry() { + this.defaultExcludePatterns.add("/actuator/health/**"); + this.defaultExcludePatterns.add("/v3/api-docs/**"); + this.defaultExcludePatterns.add("/swagger-ui/**"); + this.defaultExcludePatterns.add("/oauth/**"); + this.defaultExcludePatterns.add("/feign/client/**"); + this.defaultExcludePatterns.add("/process/resource-view"); + this.defaultExcludePatterns.add("/process/diagram-view"); + this.defaultExcludePatterns.add("/manager/check-upload"); + this.defaultExcludePatterns.add("/tenant/info"); + this.defaultExcludePatterns.add("/static/**"); + this.defaultExcludePatterns.add("/assets/**"); + this.defaultExcludePatterns.add("/error"); + this.defaultExcludePatterns.add("/favicon.ico"); + } + + /** + * 设置单个放行api + */ + public SecureRegistry excludePathPattern(String pattern) { + this.excludePatterns.add(pattern); + return this; + } + + /** + * 设置放行api集合 + */ + public SecureRegistry excludePathPatterns(String... patterns) { + this.excludePatterns.addAll(Arrays.asList(patterns)); + return this; + } + + /** + * 设置放行api集合 + */ + public void excludePathPatterns(List patterns) { + this.excludePatterns.addAll(patterns); + } + + /** + * 设置单个自定义授权 + */ + public SecureRegistry addAuthPattern(HttpMethod method, String pattern, String expression) { + this.authSecures.add(new AuthSecure(method, pattern, expression)); + return this; + } + + /** + * 设置自定义授权集合 + */ + public SecureRegistry addAuthPatterns(List authSecures) { + this.authSecures.addAll(authSecures); + return this; + } + + /** + * 设置基础认证 + */ + public SecureRegistry addBasicPattern(HttpMethod method, String pattern, String username, String password) { + this.basicSecures.add(new BasicSecure(method, pattern, username, password)); + return this; + } + + /** + * 设置基础认证集合 + */ + public SecureRegistry addBasicPatterns(List basicSecures) { + this.basicSecures.addAll(basicSecures); + return this; + } + + /** + * 设置签名认证 + */ + public SecureRegistry addSignPattern(HttpMethod method, String pattern, String crypto) { + this.signSecures.add(new SignSecure(method, pattern, crypto)); + return this; + } + + /** + * 设置签名认证集合 + */ + public SecureRegistry addSignPatterns(List signSecures) { + this.signSecures.addAll(signSecures); + return this; + } + + /** + * 设置是否开启令牌严格模式 + */ + public SecureRegistry strictToken(boolean strictToken) { + this.strictToken = strictToken; + return this; + } + + /** + * 设置是否开启请求头严格模式 + */ + public SecureRegistry strictHeader(boolean strictHeader) { + this.strictHeader = strictHeader; + return this; + } + +} diff --git a/blade-core-secure/src/main/java/org/springblade/core/secure/utils/SecureUtil.java b/blade-core-secure/src/main/java/org/springblade/core/secure/utils/SecureUtil.java new file mode 100644 index 0000000..d1c8c4b --- /dev/null +++ b/blade-core-secure/src/main/java/org/springblade/core/secure/utils/SecureUtil.java @@ -0,0 +1,186 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.secure.utils; + +import io.jsonwebtoken.JwtBuilder; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import lombok.SneakyThrows; +import org.springblade.core.jwt.JwtUtil; +import org.springblade.core.secure.TokenInfo; +import org.springblade.core.secure.constant.SecureConstant; +import org.springblade.core.tool.support.Kv; +import org.springblade.core.tool.utils.Charsets; +import org.springblade.core.tool.utils.Func; +import org.springblade.core.tool.utils.StringPool; +import org.springblade.core.tool.utils.WebUtil; + +import javax.crypto.SecretKey; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.ZoneId; +import java.time.temporal.ChronoUnit; +import java.util.Base64; +import java.util.Date; +import java.util.Objects; +import java.util.Optional; + +import static org.springblade.core.secure.constant.SecureConstant.*; + +/** + * Secure工具类 + * + * @author Chill + */ +public class SecureUtil extends AuthUtil { + + public static final String TYP = "typ"; + public static final String JWT = "JWT"; + public static final String AUDIENCE = "bladex"; + public static final String ISSUER = "bladex.cn"; + + /** + * 创建令牌 + * + * @param kv 构建参数 + * @return TokenInfo + */ + public static TokenInfo createToken(Kv kv) { + return createToken(kv, null, AUDIENCE, ISSUER); + } + + /** + * 创建令牌 + * + * @param kv 构建参数 + * @param expire 过期秒数 + * @return TokenInfo + */ + public static TokenInfo createToken(Kv kv, Integer expire) { + return createToken(kv, expire, AUDIENCE, ISSUER); + } + + /** + * 创建令牌 + * + * @param kv 构建参数 + * @param expire 过期秒数 + * @param audience audience + * @param issuer issuer + * @return TokenInfo + */ + public static TokenInfo createToken(Kv kv, Integer expire, String audience, String issuer) { + // 添加Token过期时间 + Instant now = Instant.now(); + int expireSeconds = Optional.ofNullable(expire) + .orElseGet(SecureUtil::getExpire); // 获取默认过期时间 + Instant exp = now.plus(expireSeconds, ChronoUnit.SECONDS); + + // 生成签名密钥 + SecretKey signingKey = Keys.hmacShaKeyFor(Base64.getDecoder().decode(JwtUtil.getBase64Security())); + + // 添加构成JWT的类 + JwtBuilder builder = Jwts.builder().header().add(TYP, JWT) + .and().issuer(issuer).audience().add(audience) + .and().signWith(signingKey); + + // 设置JWT参数 + kv.forEach(builder::claim); + + // 设置Token过期时间 + builder.expiration(Date.from(exp)).notBefore(Date.from(now)); + + // 组装Token信息 + TokenInfo tokenInfo = new TokenInfo(); + tokenInfo.setToken(builder.compact()); + tokenInfo.setExpire(expireSeconds); + + // 返回Token信息 + return tokenInfo; + } + + /** + * 获取默认过期时间(次日凌晨3点) + * + * @return expire + */ + public static int getExpire() { + LocalTime threeAM = LocalTime.of(3, 0); + LocalDate tomorrow = LocalDate.now(ZoneId.systemDefault()).plusDays(1); + Instant threeAMTomorrow = tomorrow.atTime(threeAM).atZone(ZoneId.systemDefault()).toInstant(); + return (int) ChronoUnit.SECONDS.between(Instant.now(), threeAMTomorrow); + } + + + /** + * 获取请求头中的客户端id + */ + public static String getClientId() { + String[] tokens = extractAndDecodeAuthorization(); + assert tokens.length == 2; + return tokens[0]; + } + + /** + * 获取请求头中的客户端密钥 + */ + public static String getClientSecret() { + String[] tokens = extractAndDecodeAuthorization(); + assert tokens.length == 2; + return tokens[1]; + } + + /** + * 客户端信息解码 + */ + @SneakyThrows + public static String[] extractAndDecodeAuthorization() { + // 获取请求头客户端信息 + String header = Objects.requireNonNull(WebUtil.getRequest()).getHeader(SecureConstant.BASIC_HEADER_KEY); + header = Func.toStr(header).replace(SecureConstant.BASIC_HEADER_PREFIX_EXT, SecureConstant.BASIC_HEADER_PREFIX); + if (!header.startsWith(SecureConstant.BASIC_HEADER_PREFIX)) { + throw new SecurityException(AUTHORIZATION_NOT_FOUND); + } + byte[] base64Token = header.substring(6).getBytes(Charsets.UTF_8_NAME); + + byte[] decoded; + try { + decoded = Base64.getDecoder().decode(base64Token); + } catch (IllegalArgumentException exception) { + throw new SecurityException(CLIENT_TOKEN_PARSE_FAILED); + } + + String token = new String(decoded, Charsets.UTF_8_NAME); + int index = token.indexOf(StringPool.COLON); + if (index == -1) { + throw new SecurityException(INVALID_CLIENT_TOKEN); + } else { + return new String[]{token.substring(0, index), token.substring(index + 1)}; + } + } + +} diff --git a/blade-core-test/pom.xml b/blade-core-test/pom.xml new file mode 100644 index 0000000..c0c4ae3 --- /dev/null +++ b/blade-core-test/pom.xml @@ -0,0 +1,30 @@ + + + 4.0.0 + + BladeX-Tool + org.springblade + ${revision} + + + blade-core-test + ${project.artifactId} + ${project.parent.version} + jar + + + + + org.springblade + blade-core-launch + + + + org.springframework.boot + spring-boot-starter-test + + + + diff --git a/blade-core-test/src/main/java/org/springblade/core/test/BladeBootTest.java b/blade-core-test/src/main/java/org/springblade/core/test/BladeBootTest.java new file mode 100644 index 0000000..d91d6c7 --- /dev/null +++ b/blade-core-test/src/main/java/org/springblade/core/test/BladeBootTest.java @@ -0,0 +1,69 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.test; + +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.core.annotation.AliasFor; + +import java.lang.annotation.*; + +/** + * 简化 测试 + * + * @author L.cm + */ +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@SpringBootTest +@ExtendWith(BladeSpringExtension.class) +public @interface BladeBootTest { + /** + * 服务名:appName + * @return appName + */ + @AliasFor("appName") + String value() default "blade-test"; + /** + * 服务名:appName + * @return appName + */ + @AliasFor("value") + String appName() default "blade-test"; + /** + * profile + * @return profile + */ + String profile() default "dev"; + /** + * 启用 ServiceLoader 加载 launcherService + * @return 是否启用 + */ + boolean enableLoader() default false; +} diff --git a/blade-core-test/src/main/java/org/springblade/core/test/BladeBootTestException.java b/blade-core-test/src/main/java/org/springblade/core/test/BladeBootTestException.java new file mode 100644 index 0000000..533d9ce --- /dev/null +++ b/blade-core-test/src/main/java/org/springblade/core/test/BladeBootTestException.java @@ -0,0 +1,39 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.test; + +/** + * blade test 异常 + * + * @author L.cm + */ +class BladeBootTestException extends RuntimeException { + + BladeBootTestException(String message) { + super(message); + } +} diff --git a/blade-core-test/src/main/java/org/springblade/core/test/BladeSpringExtension.java b/blade-core-test/src/main/java/org/springblade/core/test/BladeSpringExtension.java new file mode 100644 index 0000000..27537d4 --- /dev/null +++ b/blade-core-test/src/main/java/org/springblade/core/test/BladeSpringExtension.java @@ -0,0 +1,97 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.test; + + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.springblade.core.launch.BladeApplication; +import org.springblade.core.launch.constant.AppConstant; +import org.springblade.core.launch.constant.NacosConstant; +import org.springblade.core.launch.constant.SentinelConstant; +import org.springblade.core.launch.service.LauncherService; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.lang.NonNull; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * 设置启动参数 + * + * @author L.cm + */ +public class BladeSpringExtension extends SpringExtension { + + @Override + public void beforeAll(@NonNull ExtensionContext context) throws Exception { + super.beforeAll(context); + setUpTestClass(context); + } + + private void setUpTestClass(ExtensionContext context) { + Class clazz = context.getRequiredTestClass(); + BladeBootTest bladeBootTest = AnnotationUtils.getAnnotation(clazz, BladeBootTest.class); + if (bladeBootTest == null) { + throw new BladeBootTestException(String.format("%s must be @BladeBootTest .", clazz)); + } + String appName = bladeBootTest.appName(); + String profile = bladeBootTest.profile(); + boolean isLocalDev = BladeApplication.isLocalDev(); + Properties props = System.getProperties(); + props.setProperty("blade.env", profile); + props.setProperty("blade.name", appName); + props.setProperty("blade.is-local", String.valueOf(isLocalDev)); + props.setProperty("blade.dev-mode", profile.equals(AppConstant.PROD_CODE) ? "false" : "true"); + props.setProperty("blade.service.version", AppConstant.APPLICATION_VERSION); + props.setProperty("spring.application.name", appName); + props.setProperty("spring.profiles.active", profile); + props.setProperty("info.version", AppConstant.APPLICATION_VERSION); + props.setProperty("info.desc", appName); + props.setProperty("loadbalancer.client.name", appName); + props.setProperty("spring.cloud.sentinel.transport.dashboard", SentinelConstant.SENTINEL_ADDR); + props.setProperty("spring.main.allow-bean-definition-overriding", "true"); + props.setProperty("spring.cloud.nacos.config.shared-configs[0].data-id", NacosConstant.sharedDataId()); + props.setProperty("spring.cloud.nacos.config.shared-configs[0].group", NacosConstant.NACOS_CONFIG_GROUP); + props.setProperty("spring.cloud.nacos.config.shared-configs[0].refresh", NacosConstant.NACOS_CONFIG_REFRESH); + props.setProperty("spring.cloud.nacos.config.file-extension", NacosConstant.NACOS_CONFIG_FORMAT); + props.setProperty("spring.cloud.nacos.config.shared-configs[1].data-id", NacosConstant.sharedDataId(profile)); + props.setProperty("spring.cloud.nacos.config.shared-configs[1].group", NacosConstant.NACOS_CONFIG_GROUP); + props.setProperty("spring.cloud.nacos.config.shared-configs[1].refresh", NacosConstant.NACOS_CONFIG_REFRESH); + // 加载自定义组件 + if (bladeBootTest.enableLoader()) { + SpringApplicationBuilder builder = new SpringApplicationBuilder(clazz); + List launcherList = new ArrayList<>(); + ServiceLoader.load(LauncherService.class).forEach(launcherList::add); + launcherList.stream().sorted(Comparator.comparing(LauncherService::getOrder)).collect(Collectors.toList()) + .forEach(launcherService -> launcherService.launcher(builder, appName, profile, isLocalDev)); + } + System.err.printf("---[junit.test]:[%s]---启动中,读取到的环境变量:[%s]%n", appName, profile); + } + +} diff --git a/blade-core-tool/pom.xml b/blade-core-tool/pom.xml new file mode 100644 index 0000000..32c1491 --- /dev/null +++ b/blade-core-tool/pom.xml @@ -0,0 +1,78 @@ + + + 4.0.0 + + BladeX-Tool + org.springblade + ${revision} + + + blade-core-tool + ${project.artifactId} + ${project.parent.version} + jar + + + + + org.springblade + blade-core-launch + + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + + com.google.guava + guava + + + + io.swagger.core.v3 + swagger-annotations + + + + io.protostuff + protostuff-core + + + io.protostuff + protostuff-runtime + + + + com.fasterxml.jackson.module + jackson-module-jaxb-annotations + + + + org.springframework.boot + spring-boot-starter-validation + + + + javax.xml.bind + jaxb-api + + + com.sun.xml.bind + jaxb-core + + + com.sun.xml.bind + jaxb-impl + + + + org.springblade + blade-core-auto + provided + + + + diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/api/IResultCode.java b/blade-core-tool/src/main/java/org/springblade/core/tool/api/IResultCode.java new file mode 100644 index 0000000..ca0b570 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/api/IResultCode.java @@ -0,0 +1,51 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.tool.api; + +import java.io.Serializable; + +/** + * 业务代码接口 + * + * @author Chill + */ +public interface IResultCode extends Serializable { + + /** + * 获取消息 + * + * @return + */ + String getMessage(); + + /** + * 获取状态码 + * + * @return + */ + int getCode(); + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/api/R.java b/blade-core-tool/src/main/java/org/springblade/core/tool/api/R.java new file mode 100644 index 0000000..e136dda --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/api/R.java @@ -0,0 +1,238 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.tool.api; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.servlet.http.HttpServletResponse; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import org.springblade.core.tool.constant.BladeConstant; +import org.springblade.core.tool.utils.ObjectUtil; +import org.springframework.lang.Nullable; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Optional; + +/** + * 统一API响应结果封装 + * + * @author Chill + */ +@Getter +@Setter +@ToString +@Schema(description = "返回信息") +@NoArgsConstructor +public class R implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "状态码", requiredMode = Schema.RequiredMode.REQUIRED) + private int code; + @Schema(description = "是否成功", requiredMode = Schema.RequiredMode.REQUIRED) + private boolean success; + @Schema(description = "承载数据") + private T data; + @Schema(description = "返回消息", requiredMode = Schema.RequiredMode.REQUIRED) + private String msg; + + private R(IResultCode resultCode) { + this(resultCode, null, resultCode.getMessage()); + } + + private R(IResultCode resultCode, String msg) { + this(resultCode, null, msg); + } + + private R(IResultCode resultCode, T data) { + this(resultCode, data, resultCode.getMessage()); + } + + private R(IResultCode resultCode, T data, String msg) { + this(resultCode.getCode(), data, msg); + } + + private R(int code, T data, String msg) { + this.code = code; + this.data = data; + this.msg = msg; + this.success = ResultCode.SUCCESS.code == code; + } + + /** + * 判断返回是否为成功 + * + * @param result Result + * @return 是否成功 + */ + public static boolean isSuccess(@Nullable R result) { + return Optional.ofNullable(result) + .map(x -> ObjectUtil.nullSafeEquals(ResultCode.SUCCESS.code, x.code)) + .orElse(Boolean.FALSE); + } + + /** + * 判断返回是否为成功 + * + * @param result Result + * @return 是否成功 + */ + public static boolean isNotSuccess(@Nullable R result) { + return !R.isSuccess(result); + } + + /** + * 返回R + * + * @param data 数据 + * @param T 泛型标记 + * @return R + */ + public static R data(T data) { + return data(data, BladeConstant.DEFAULT_SUCCESS_MESSAGE); + } + + /** + * 返回R + * + * @param data 数据 + * @param msg 消息 + * @param T 泛型标记 + * @return R + */ + public static R data(T data, String msg) { + return data(HttpServletResponse.SC_OK, data, msg); + } + + /** + * 返回R + * + * @param code 状态码 + * @param data 数据 + * @param msg 消息 + * @param T 泛型标记 + * @return R + */ + public static R data(int code, T data, String msg) { + return new R<>(code, data, data == null ? BladeConstant.DEFAULT_NULL_MESSAGE : msg); + } + + /** + * 返回R + * + * @param msg 消息 + * @param T 泛型标记 + * @return R + */ + public static R success(String msg) { + return new R<>(ResultCode.SUCCESS, msg); + } + + /** + * 返回R + * + * @param resultCode 业务代码 + * @param T 泛型标记 + * @return R + */ + public static R success(IResultCode resultCode) { + return new R<>(resultCode); + } + + /** + * 返回R + * + * @param resultCode 业务代码 + * @param msg 消息 + * @param T 泛型标记 + * @return R + */ + public static R success(IResultCode resultCode, String msg) { + return new R<>(resultCode, msg); + } + + /** + * 返回R + * + * @param msg 消息 + * @param T 泛型标记 + * @return R + */ + public static R fail(String msg) { + return new R<>(ResultCode.FAILURE, msg); + } + + + /** + * 返回R + * + * @param code 状态码 + * @param msg 消息 + * @param T 泛型标记 + * @return R + */ + public static R fail(int code, String msg) { + return new R<>(code, null, msg); + } + + /** + * 返回R + * + * @param resultCode 业务代码 + * @param T 泛型标记 + * @return R + */ + public static R fail(IResultCode resultCode) { + return new R<>(resultCode); + } + + /** + * 返回R + * + * @param resultCode 业务代码 + * @param msg 消息 + * @param T 泛型标记 + * @return R + */ + public static R fail(IResultCode resultCode, String msg) { + return new R<>(resultCode, msg); + } + + /** + * 返回R + * + * @param flag 成功状态 + * @return R + */ + public static R status(boolean flag) { + return flag ? success(BladeConstant.DEFAULT_SUCCESS_MESSAGE) : fail(BladeConstant.DEFAULT_FAILURE_MESSAGE); + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/api/ResultCode.java b/blade-core-tool/src/main/java/org/springblade/core/tool/api/ResultCode.java new file mode 100644 index 0000000..67e455b --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/api/ResultCode.java @@ -0,0 +1,122 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.tool.api; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import jakarta.servlet.http.HttpServletResponse; + +/** + * 业务代码枚举 + * + * @author Chill + */ +@Getter +@AllArgsConstructor +public enum ResultCode implements IResultCode { + + /** + * 操作成功 + */ + SUCCESS(HttpServletResponse.SC_OK, "操作成功"), + + /** + * 业务异常 + */ + FAILURE(HttpServletResponse.SC_BAD_REQUEST, "业务异常"), + + /** + * 请求未授权 + */ + UN_AUTHORIZED(HttpServletResponse.SC_UNAUTHORIZED, "请求未授权"), + + /** + * 客户端请求未授权 + */ + CLIENT_UN_AUTHORIZED(HttpServletResponse.SC_UNAUTHORIZED, "客户端请求未授权"), + + /** + * 404 没找到请求 + */ + NOT_FOUND(HttpServletResponse.SC_NOT_FOUND, "404 没找到请求"), + + /** + * 消息不能读取 + */ + MSG_NOT_READABLE(HttpServletResponse.SC_BAD_REQUEST, "消息不能读取"), + + /** + * 不支持当前请求方法 + */ + METHOD_NOT_SUPPORTED(HttpServletResponse.SC_METHOD_NOT_ALLOWED, "不支持当前请求方法"), + + /** + * 不支持当前媒体类型 + */ + MEDIA_TYPE_NOT_SUPPORTED(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE, "不支持当前媒体类型"), + + /** + * 请求被拒绝 + */ + REQ_REJECT(HttpServletResponse.SC_FORBIDDEN, "请求被拒绝"), + + /** + * 服务器异常 + */ + INTERNAL_SERVER_ERROR(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "服务器异常"), + + /** + * 缺少必要的请求参数 + */ + PARAM_MISS(HttpServletResponse.SC_BAD_REQUEST, "缺少必要的请求参数"), + + /** + * 请求参数类型错误 + */ + PARAM_TYPE_ERROR(HttpServletResponse.SC_BAD_REQUEST, "请求参数类型错误"), + + /** + * 请求参数绑定错误 + */ + PARAM_BIND_ERROR(HttpServletResponse.SC_BAD_REQUEST, "请求参数绑定错误"), + + /** + * 参数校验失败 + */ + PARAM_VALID_ERROR(HttpServletResponse.SC_BAD_REQUEST, "参数校验失败"), + ; + + /** + * code编码 + */ + final int code; + /** + * 中文信息描述 + */ + final String message; + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/beans/BeanProperty.java b/blade-core-tool/src/main/java/org/springblade/core/tool/beans/BeanProperty.java new file mode 100644 index 0000000..4ca9c0c --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/beans/BeanProperty.java @@ -0,0 +1,16 @@ +package org.springblade.core.tool.beans; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * Bean属性 + * + * @author Chill + */ +@Getter +@AllArgsConstructor +public class BeanProperty { + private final String name; + private final Class type; +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/beans/BladeBeanCopier.java b/blade-core-tool/src/main/java/org/springblade/core/tool/beans/BladeBeanCopier.java new file mode 100644 index 0000000..bfcc22e --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/beans/BladeBeanCopier.java @@ -0,0 +1,414 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.tool.beans; + + +import org.springblade.core.tool.utils.BeanUtil; +import org.springblade.core.tool.utils.ClassUtil; +import org.springblade.core.tool.utils.ReflectUtil; +import org.springblade.core.tool.utils.StringUtil; +import org.springframework.asm.ClassVisitor; +import org.springframework.asm.Label; +import org.springframework.asm.Opcodes; +import org.springframework.asm.Type; +import org.springframework.cglib.core.*; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; + +import java.beans.PropertyDescriptor; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.security.ProtectionDomain; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * spring cglib 魔改 + * + *

+ * 1. 支持链式 bean,支持 map + * 2. ClassLoader 跟 target 保持一致 + *

+ * + * @author L.cm + */ +public abstract class BladeBeanCopier { + private static final Type CONVERTER = TypeUtils.parseType("org.springframework.cglib.core.Converter"); + private static final Type BEAN_COPIER = TypeUtils.parseType(BladeBeanCopier.class.getName()); + private static final Type BEAN_MAP = TypeUtils.parseType(Map.class.getName()); + private static final Signature COPY = new Signature("copy", Type.VOID_TYPE, new Type[]{Constants.TYPE_OBJECT, Constants.TYPE_OBJECT, CONVERTER}); + private static final Signature CONVERT = TypeUtils.parseSignature("Object convert(Object, Class, Object)"); + private static final Signature BEAN_MAP_GET = TypeUtils.parseSignature("Object get(Object)"); + private static final Type CLASS_UTILS = TypeUtils.parseType(ClassUtils.class.getName()); + private static final Signature IS_ASSIGNABLE_VALUE = TypeUtils.parseSignature("boolean isAssignableValue(Class, Object)"); + /** + * The map to store {@link BladeBeanCopier} of source type and class type for copy. + */ + private static final ConcurrentMap BEAN_COPIER_MAP = new ConcurrentHashMap<>(); + + public static BladeBeanCopier create(Class source, Class target, boolean useConverter) { + return BladeBeanCopier.create(source, target, useConverter, false); + } + + public static BladeBeanCopier create(Class source, Class target, boolean useConverter, boolean nonNull) { + BladeBeanCopierKey copierKey = new BladeBeanCopierKey(source, target, useConverter, nonNull); + // 利用 ConcurrentMap 缓存 提高性能,接近 直接 get set + return BEAN_COPIER_MAP.computeIfAbsent(copierKey, key -> { + Generator gen = new Generator(); + gen.setSource(key.getSource()); + gen.setTarget(key.getTarget()); + gen.setUseConverter(key.isUseConverter()); + gen.setNonNull(key.isNonNull()); + return gen.create(key); + }); + } + + /** + * Bean copy + * + * @param from from Bean + * @param to to Bean + * @param converter Converter + */ + abstract public void copy(Object from, Object to, @Nullable Converter converter); + + public static class Generator extends AbstractClassGenerator { + private static final Source SOURCE = new Source(BladeBeanCopier.class.getName()); + private Class source; + private Class target; + private boolean useConverter; + private boolean nonNull; + + Generator() { + super(SOURCE); + } + + public void setSource(Class source) { + if (!Modifier.isPublic(source.getModifiers())) { + setNamePrefix(source.getName()); + } + this.source = source; + } + + public void setTarget(Class target) { + if (!Modifier.isPublic(target.getModifiers())) { + setNamePrefix(target.getName()); + } + this.target = target; + } + + public void setUseConverter(boolean useConverter) { + this.useConverter = useConverter; + } + + public void setNonNull(boolean nonNull) { + this.nonNull = nonNull; + } + + @Override + protected ClassLoader getDefaultClassLoader() { + // L.cm 保证 和 返回使用同一个 ClassLoader + return target.getClassLoader(); + } + + @Override + protected ProtectionDomain getProtectionDomain() { + return ReflectUtils.getProtectionDomain(source); + } + + @Override + public BladeBeanCopier create(Object key) { + return (BladeBeanCopier) super.create(key); + } + + @Override + public void generateClass(ClassVisitor v) { + Type sourceType = Type.getType(source); + Type targetType = Type.getType(target); + ClassEmitter ce = new ClassEmitter(v); + ce.begin_class(Constants.V1_2, + Constants.ACC_PUBLIC, + getClassName(), + BEAN_COPIER, + null, + Constants.SOURCE_FILE); + + EmitUtils.null_constructor(ce); + CodeEmitter e = ce.begin_method(Constants.ACC_PUBLIC, COPY, null); + + // map 单独处理 + if (Map.class.isAssignableFrom(source)) { + generateClassFormMap(ce, e, sourceType, targetType); + return; + } + + // 2018.12.27 by L.cm 支持链式 bean + // 注意:此处需兼容链式bean 使用了 spring 的方法,比较耗时 + PropertyDescriptor[] getters = ReflectUtil.getBeanGetters(source); + PropertyDescriptor[] setters = ReflectUtil.getBeanSetters(target); + Map names = new HashMap<>(16); + for (PropertyDescriptor getter : getters) { + names.put(getter.getName(), getter); + } + + Local targetLocal = e.make_local(); + Local sourceLocal = e.make_local(); + e.load_arg(1); + e.checkcast(targetType); + e.store_local(targetLocal); + e.load_arg(0); + e.checkcast(sourceType); + e.store_local(sourceLocal); + + for (PropertyDescriptor setter : setters) { + String propName = setter.getName(); + + CopyProperty targetIgnoreCopy = ReflectUtil.getAnnotation(target, propName, CopyProperty.class); + // set 上有忽略的 注解 + if (targetIgnoreCopy != null) { + if (targetIgnoreCopy.ignore()) { + continue; + } + // 注解上的别名,如果别名不为空,使用别名 + String aliasTargetPropName = targetIgnoreCopy.value(); + if (StringUtil.isNotBlank(aliasTargetPropName)) { + propName = aliasTargetPropName; + } + } + // 找到对应的 get + PropertyDescriptor getter = names.get(propName); + // 没有 get 跳出 + if (getter == null) { + continue; + } + + MethodInfo read = ReflectUtils.getMethodInfo(getter.getReadMethod()); + Method writeMethod = setter.getWriteMethod(); + MethodInfo write = ReflectUtils.getMethodInfo(writeMethod); + Type returnType = read.getSignature().getReturnType(); + Type setterType = write.getSignature().getArgumentTypes()[0]; + Class getterPropertyType = getter.getPropertyType(); + Class setterPropertyType = setter.getPropertyType(); + + // L.cm 2019.01.12 优化逻辑,先判断类型,类型一致直接 set,不同再判断 是否 类型转换 + // nonNull Label + Label l0 = e.make_label(); + // 判断类型是否一致,包括 包装类型 + if (ClassUtil.isAssignable(setterPropertyType, getterPropertyType)) { + // 2018.12.27 by L.cm 支持链式 bean + e.load_local(targetLocal); + e.load_local(sourceLocal); + e.invoke(read); + boolean getterIsPrimitive = getterPropertyType.isPrimitive(); + boolean setterIsPrimitive = setterPropertyType.isPrimitive(); + + if (nonNull) { + // 需要落栈,强制装箱 + e.box(returnType); + Local var = e.make_local(); + e.store_local(var); + e.load_local(var); + // nonNull Label + e.ifnull(l0); + e.load_local(targetLocal); + e.load_local(var); + // 需要落栈,强制拆箱 + e.unbox_or_zero(setterType); + } else { + // 如果 get 为原始类型,需要装箱 + if (getterIsPrimitive && !setterIsPrimitive) { + e.box(returnType); + } + // 如果 set 为原始类型,需要拆箱 + if (!getterIsPrimitive && setterIsPrimitive) { + e.unbox_or_zero(setterType); + } + } + + // 构造 set 方法 + invokeWrite(e, write, writeMethod, nonNull, l0); + } else if (useConverter) { + e.load_local(targetLocal); + e.load_arg(2); + e.load_local(sourceLocal); + e.invoke(read); + e.box(returnType); + + if (nonNull) { + Local var = e.make_local(); + e.store_local(var); + e.load_local(var); + e.ifnull(l0); + e.load_local(targetLocal); + e.load_arg(2); + e.load_local(var); + } + + EmitUtils.load_class(e, setterType); + // 更改成了属性名,之前是 set 方法名 + e.push(propName); + e.invoke_interface(CONVERTER, CONVERT); + e.unbox_or_zero(setterType); + + // 构造 set 方法 + invokeWrite(e, write, writeMethod, nonNull, l0); + } + } + e.return_value(); + e.end_method(); + ce.end_class(); + } + + private static void invokeWrite(CodeEmitter e, MethodInfo write, Method writeMethod, boolean nonNull, Label l0) { + // 返回值,判断 链式 bean + Class returnType = writeMethod.getReturnType(); + e.invoke(write); + // 链式 bean,有返回值需要 pop + if (!returnType.equals(Void.TYPE)) { + e.pop(); + } + if (nonNull) { + e.visitLabel(l0); + } + } + + @Override + protected Object firstInstance(Class type) { + return BeanUtil.newInstance(type); + } + + @Override + protected Object nextInstance(Object instance) { + return instance; + } + + /** + * 处理 map 的 copy + * @param ce ClassEmitter + * @param e CodeEmitter + * @param sourceType sourceType + * @param targetType targetType + */ + public void generateClassFormMap(ClassEmitter ce, CodeEmitter e, Type sourceType, Type targetType) { + // 2018.12.27 by L.cm 支持链式 bean + PropertyDescriptor[] setters = ReflectUtil.getBeanSetters(target); + + // 入口变量 + Local targetLocal = e.make_local(); + Local sourceLocal = e.make_local(); + e.load_arg(1); + e.checkcast(targetType); + e.store_local(targetLocal); + e.load_arg(0); + e.checkcast(sourceType); + e.store_local(sourceLocal); + Type mapBox = Type.getType(Object.class); + + for (PropertyDescriptor setter : setters) { + String propName = setter.getName(); + + // set 上有忽略的 注解 + CopyProperty targetIgnoreCopy = ReflectUtil.getAnnotation(target, propName, CopyProperty.class); + if (targetIgnoreCopy != null) { + if (targetIgnoreCopy.ignore()) { + continue; + } + // 注解上的别名 + String aliasTargetPropName = targetIgnoreCopy.value(); + if (StringUtil.isNotBlank(aliasTargetPropName)) { + propName = aliasTargetPropName; + } + } + + Method writeMethod = setter.getWriteMethod(); + MethodInfo write = ReflectUtils.getMethodInfo(writeMethod); + Type setterType = write.getSignature().getArgumentTypes()[0]; + + e.load_local(targetLocal); + e.load_local(sourceLocal); + + e.push(propName); + // 执行 map get + e.invoke_interface(BEAN_MAP, BEAN_MAP_GET); + // box 装箱,避免 array[] 数组问题 + e.box(mapBox); + + // 生成变量 + Local var = e.make_local(); + e.store_local(var); + e.load_local(var); + + // 先判断 不为null,然后做类型判断 + Label l0 = e.make_label(); + e.ifnull(l0); + EmitUtils.load_class(e, setterType); + e.load_local(var); + // ClassUtils.isAssignableValue(Integer.class, id) + e.invoke_static(CLASS_UTILS, IS_ASSIGNABLE_VALUE); + Label l1 = new Label(); + // 返回值,判断 链式 bean + Class returnType = writeMethod.getReturnType(); + if (useConverter) { + e.if_jump(Opcodes.IFEQ, l1); + e.load_local(targetLocal); + e.load_local(var); + e.unbox_or_zero(setterType); + e.invoke(write); + if (!returnType.equals(Void.TYPE)) { + e.pop(); + } + e.goTo(l0); + e.visitLabel(l1); + e.load_local(targetLocal); + e.load_arg(2); + e.load_local(var); + EmitUtils.load_class(e, setterType); + e.push(propName); + e.invoke_interface(CONVERTER, CONVERT); + e.unbox_or_zero(setterType); + e.invoke(write); + } else { + e.if_jump(Opcodes.IFEQ, l0); + e.load_local(targetLocal); + e.load_local(var); + e.unbox_or_zero(setterType); + e.invoke(write); + } + // 返回值,判断 链式 bean + if (!returnType.equals(Void.TYPE)) { + e.pop(); + } + e.visitLabel(l0); + } + e.return_value(); + e.end_method(); + ce.end_class(); + } + } +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/beans/BladeBeanCopierKey.java b/blade-core-tool/src/main/java/org/springblade/core/tool/beans/BladeBeanCopierKey.java new file mode 100644 index 0000000..af10c54 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/beans/BladeBeanCopierKey.java @@ -0,0 +1,20 @@ +package org.springblade.core.tool.beans; + +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +/** + * copy key + * + * @author L.cm + */ +@Getter +@EqualsAndHashCode +@AllArgsConstructor +public class BladeBeanCopierKey { + private final Class source; + private final Class target; + private final boolean useConverter; + private final boolean nonNull; +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/beans/BladeBeanMap.java b/blade-core-tool/src/main/java/org/springblade/core/tool/beans/BladeBeanMap.java new file mode 100644 index 0000000..f2823fb --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/beans/BladeBeanMap.java @@ -0,0 +1,125 @@ +package org.springblade.core.tool.beans; + +import org.springframework.asm.ClassVisitor; +import org.springframework.cglib.beans.BeanMap; +import org.springframework.cglib.core.AbstractClassGenerator; +import org.springframework.cglib.core.ReflectUtils; + +import java.security.ProtectionDomain; + +/** + * 重写 cglib BeanMap,支持链式bean + * + * @author L.cm + */ +public abstract class BladeBeanMap extends BeanMap { + protected BladeBeanMap() { + } + + protected BladeBeanMap(Object bean) { + super(bean); + } + + public static BladeBeanMap create(Object bean) { + BladeGenerator gen = new BladeGenerator(); + gen.setBean(bean); + return gen.create(); + } + + /** + * newInstance + * + * @param o Object + * @return BladeBeanMap + */ + @Override + public abstract BladeBeanMap newInstance(Object o); + + public static class BladeGenerator extends AbstractClassGenerator { + private static final Source SOURCE = new Source(BladeBeanMap.class.getName()); + + private Object bean; + private Class beanClass; + private int require; + + public BladeGenerator() { + super(SOURCE); + } + + /** + * Set the bean that the generated map should reflect. The bean may be swapped + * out for another bean of the same type using {@link #setBean}. + * Calling this method overrides any value previously set using {@link #setBeanClass}. + * You must call either this method or {@link #setBeanClass} before {@link #create}. + * + * @param bean the initial bean + */ + public void setBean(Object bean) { + this.bean = bean; + if (bean != null) { + beanClass = bean.getClass(); + } + } + + /** + * Set the class of the bean that the generated map should support. + * You must call either this method or {@link #setBeanClass} before {@link #create}. + * + * @param beanClass the class of the bean + */ + public void setBeanClass(Class beanClass) { + this.beanClass = beanClass; + } + + /** + * Limit the properties reflected by the generated map. + * + * @param require any combination of {@link #REQUIRE_GETTER} and + * {@link #REQUIRE_SETTER}; default is zero (any property allowed) + */ + public void setRequire(int require) { + this.require = require; + } + + @Override + protected ClassLoader getDefaultClassLoader() { + return beanClass.getClassLoader(); + } + + @Override + protected ProtectionDomain getProtectionDomain() { + return ReflectUtils.getProtectionDomain(beanClass); + } + + /** + * Create a new instance of the BeanMap. An existing + * generated class will be reused if possible. + * + * @return {BladeBeanMap} + */ + public BladeBeanMap create() { + if (beanClass == null) { + throw new IllegalArgumentException("Class of bean unknown"); + } + setNamePrefix(beanClass.getName()); + BladeBeanMapKey key = new BladeBeanMapKey(beanClass, require); + return (BladeBeanMap) super.create(key); + } + + @Override + public void generateClass(ClassVisitor v) throws Exception { + new BladeBeanMapEmitter(v, getClassName(), beanClass, require); + } + + @Override + protected Object firstInstance(Class type) { + return ((BeanMap) ReflectUtils.newInstance(type)).newInstance(bean); + } + + @Override + protected Object nextInstance(Object instance) { + return ((BeanMap) instance).newInstance(bean); + } + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/beans/BladeBeanMapEmitter.java b/blade-core-tool/src/main/java/org/springblade/core/tool/beans/BladeBeanMapEmitter.java new file mode 100644 index 0000000..8455905 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/beans/BladeBeanMapEmitter.java @@ -0,0 +1,192 @@ +package org.springblade.core.tool.beans; + +import org.springblade.core.tool.utils.ReflectUtil; +import org.springframework.asm.ClassVisitor; +import org.springframework.asm.Label; +import org.springframework.asm.Type; +import org.springframework.cglib.core.*; + +import java.beans.PropertyDescriptor; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +/** + * 重写 cglib BeanMap 处理器 + * + * @author L.cm + */ +class BladeBeanMapEmitter extends ClassEmitter { + private static final Type BEAN_MAP = TypeUtils.parseType(BladeBeanMap.class.getName()); + private static final Type FIXED_KEY_SET = TypeUtils.parseType("org.springframework.cglib.beans.FixedKeySet"); + private static final Signature CSTRUCT_OBJECT = TypeUtils.parseConstructor("Object"); + private static final Signature CSTRUCT_STRING_ARRAY = TypeUtils.parseConstructor("String[]"); + private static final Signature BEAN_MAP_GET = TypeUtils.parseSignature("Object get(Object, Object)"); + private static final Signature BEAN_MAP_PUT = TypeUtils.parseSignature("Object put(Object, Object, Object)"); + private static final Signature KEY_SET = TypeUtils.parseSignature("java.util.Set keySet()"); + private static final Signature NEW_INSTANCE = new Signature("newInstance", BEAN_MAP, new Type[]{Constants.TYPE_OBJECT}); + private static final Signature GET_PROPERTY_TYPE = TypeUtils.parseSignature("Class getPropertyType(String)"); + + public BladeBeanMapEmitter(ClassVisitor v, String className, Class type, int require) { + super(v); + + begin_class(Constants.V1_2, Constants.ACC_PUBLIC, className, BEAN_MAP, null, Constants.SOURCE_FILE); + EmitUtils.null_constructor(this); + EmitUtils.factory_method(this, NEW_INSTANCE); + generateConstructor(); + + Map getters = makePropertyMap(ReflectUtil.getBeanGetters(type)); + Map setters = makePropertyMap(ReflectUtil.getBeanSetters(type)); + Map allProps = new HashMap<>(32); + allProps.putAll(getters); + allProps.putAll(setters); + + if (require != 0) { + for (Iterator it = allProps.keySet().iterator(); it.hasNext(); ) { + String name = (String) it.next(); + if ((((require & BladeBeanMap.REQUIRE_GETTER) != 0) && !getters.containsKey(name)) || + (((require & BladeBeanMap.REQUIRE_SETTER) != 0) && !setters.containsKey(name))) { + it.remove(); + getters.remove(name); + setters.remove(name); + } + } + } + generateGet(type, getters); + generatePut(type, setters); + + String[] allNames = getNames(allProps); + generateKeySet(allNames); + generateGetPropertyType(allProps, allNames); + end_class(); + } + + private Map makePropertyMap(PropertyDescriptor[] props) { + Map names = new HashMap<>(16); + for (PropertyDescriptor prop : props) { + String propName = prop.getName(); + // 过滤 getClass,Spring 的工具类会拿到该方法 + if (!"class".equals(propName)) { + names.put(propName, prop); + } + } + return names; + } + + private String[] getNames(Map propertyMap) { + return propertyMap.keySet().toArray(new String[0]); + } + + private void generateConstructor() { + CodeEmitter e = begin_method(Constants.ACC_PUBLIC, CSTRUCT_OBJECT, null); + e.load_this(); + e.load_arg(0); + e.super_invoke_constructor(CSTRUCT_OBJECT); + e.return_value(); + e.end_method(); + } + + private void generateGet(Class type, final Map getters) { + final CodeEmitter e = begin_method(Constants.ACC_PUBLIC, BEAN_MAP_GET, null); + e.load_arg(0); + e.checkcast(Type.getType(type)); + e.load_arg(1); + e.checkcast(Constants.TYPE_STRING); + EmitUtils.string_switch(e, getNames(getters), Constants.SWITCH_STYLE_HASH, new ObjectSwitchCallback() { + @Override + public void processCase(Object key, Label end) { + PropertyDescriptor pd = getters.get(key); + MethodInfo method = ReflectUtils.getMethodInfo(pd.getReadMethod()); + e.invoke(method); + e.box(method.getSignature().getReturnType()); + e.return_value(); + } + + @Override + public void processDefault() { + e.aconst_null(); + e.return_value(); + } + }); + e.end_method(); + } + + private void generatePut(Class type, final Map setters) { + final CodeEmitter e = begin_method(Constants.ACC_PUBLIC, BEAN_MAP_PUT, null); + e.load_arg(0); + e.checkcast(Type.getType(type)); + e.load_arg(1); + e.checkcast(Constants.TYPE_STRING); + EmitUtils.string_switch(e, getNames(setters), Constants.SWITCH_STYLE_HASH, new ObjectSwitchCallback() { + @Override + public void processCase(Object key, Label end) { + PropertyDescriptor pd = setters.get(key); + if (pd.getReadMethod() == null) { + e.aconst_null(); + } else { + MethodInfo read = ReflectUtils.getMethodInfo(pd.getReadMethod()); + e.dup(); + e.invoke(read); + e.box(read.getSignature().getReturnType()); + } + // move old value behind bean + e.swap(); + // new value + e.load_arg(2); + MethodInfo write = ReflectUtils.getMethodInfo(pd.getWriteMethod()); + e.unbox(write.getSignature().getArgumentTypes()[0]); + e.invoke(write); + e.return_value(); + } + + @Override + public void processDefault() { + // fall-through + } + }); + e.aconst_null(); + e.return_value(); + e.end_method(); + } + + private void generateKeySet(String[] allNames) { + // static initializer + declare_field(Constants.ACC_STATIC | Constants.ACC_PRIVATE, "keys", FIXED_KEY_SET, null); + + CodeEmitter e = begin_static(); + e.new_instance(FIXED_KEY_SET); + e.dup(); + EmitUtils.push_array(e, allNames); + e.invoke_constructor(FIXED_KEY_SET, CSTRUCT_STRING_ARRAY); + e.putfield("keys"); + e.return_value(); + e.end_method(); + + // keySet + e = begin_method(Constants.ACC_PUBLIC, KEY_SET, null); + e.load_this(); + e.getfield("keys"); + e.return_value(); + e.end_method(); + } + + private void generateGetPropertyType(final Map allProps, String[] allNames) { + final CodeEmitter e = begin_method(Constants.ACC_PUBLIC, GET_PROPERTY_TYPE, null); + e.load_arg(0); + EmitUtils.string_switch(e, allNames, Constants.SWITCH_STYLE_HASH, new ObjectSwitchCallback() { + @Override + public void processCase(Object key, Label end) { + PropertyDescriptor pd = (PropertyDescriptor) allProps.get(key); + EmitUtils.load_class(e, Type.getType(pd.getPropertyType())); + e.return_value(); + } + + @Override + public void processDefault() { + e.aconst_null(); + e.return_value(); + } + }); + e.end_method(); + } +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/beans/BladeBeanMapKey.java b/blade-core-tool/src/main/java/org/springblade/core/tool/beans/BladeBeanMapKey.java new file mode 100644 index 0000000..6ca0fb9 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/beans/BladeBeanMapKey.java @@ -0,0 +1,16 @@ +package org.springblade.core.tool.beans; + +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; + +/** + * bean map key,提高性能 + * + * @author L.cm + */ +@EqualsAndHashCode +@AllArgsConstructor +public class BladeBeanMapKey { + private final Class type; + private final int require; +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/beans/CopyProperty.java b/blade-core-tool/src/main/java/org/springblade/core/tool/beans/CopyProperty.java new file mode 100644 index 0000000..714ee1b --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/beans/CopyProperty.java @@ -0,0 +1,26 @@ +package org.springblade.core.tool.beans; + +import java.lang.annotation.*; + +/** + * copy 字段 配置 + * + * @author L.cm + */ +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface CopyProperty { + + /** + * 属性名,用于指定别名,默认使用:field name + * @return 属性名 + */ + String value() default ""; + + /** + * 忽略:默认为 false + * @return 是否忽略 + */ + boolean ignore() default false; +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/config/BladeConverterConfiguration.java b/blade-core-tool/src/main/java/org/springblade/core/tool/config/BladeConverterConfiguration.java new file mode 100644 index 0000000..4bd88d5 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/config/BladeConverterConfiguration.java @@ -0,0 +1,23 @@ +package org.springblade.core.tool.config; + +import org.springblade.core.tool.convert.EnumToStringConverter; +import org.springblade.core.tool.convert.StringToEnumConverter; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.format.FormatterRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +/** + * blade enum 《-》 String 转换配置 + * + * @author L.cm + */ +@AutoConfiguration +public class BladeConverterConfiguration implements WebMvcConfigurer { + + @Override + public void addFormatters(FormatterRegistry registry) { + registry.addConverter(new EnumToStringConverter()); + registry.addConverter(new StringToEnumConverter()); + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/config/JacksonConfiguration.java b/blade-core-tool/src/main/java/org/springblade/core/tool/config/JacksonConfiguration.java new file mode 100644 index 0000000..b485325 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/config/JacksonConfiguration.java @@ -0,0 +1,91 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.tool.config; + +import com.fasterxml.jackson.core.json.JsonReadFeature; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import org.springblade.core.tool.jackson.BladeJacksonProperties; +import org.springblade.core.tool.jackson.BladeJavaTimeModule; +import org.springblade.core.tool.utils.DateUtil; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; + +import java.text.SimpleDateFormat; +import java.time.ZoneId; +import java.util.Locale; +import java.util.TimeZone; + +/** + * Jackson配置类 + * + * @author Chill + */ +@AutoConfiguration(before = JacksonAutoConfiguration.class) +@ConditionalOnClass(ObjectMapper.class) +@EnableConfigurationProperties(BladeJacksonProperties.class) +public class JacksonConfiguration { + + @Bean + @ConditionalOnMissingBean + public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) { + builder.simpleDateFormat(DateUtil.PATTERN_DATETIME); + //创建ObjectMapper + ObjectMapper objectMapper = builder.createXmlMapper(false).build(); + //设置地点为中国 + objectMapper.setLocale(Locale.CHINA); + //去掉默认的时间戳格式 + objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); + //设置为中国上海时区 + objectMapper.setTimeZone(TimeZone.getTimeZone(ZoneId.systemDefault())); + //序列化时,日期的统一格式 + objectMapper.setDateFormat(new SimpleDateFormat(DateUtil.PATTERN_DATETIME, Locale.CHINA)); + //序列化处理 + objectMapper.configure(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature(), true); + objectMapper.configure(JsonReadFeature.ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER.mappedFeature(), true); + //失败处理 + objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + //单引号处理 + objectMapper.configure(JsonReadFeature.ALLOW_SINGLE_QUOTES.mappedFeature(), true); + //反序列化时,属性不存在的兼容处理 + objectMapper.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + //日期格式化 + objectMapper.configure(MapperFeature.IGNORE_DUPLICATE_MODULE_REGISTRATIONS, false); + objectMapper.registerModule(BladeJavaTimeModule.INSTANCE); + objectMapper.configure(MapperFeature.IGNORE_DUPLICATE_MODULE_REGISTRATIONS, true); + objectMapper.findAndRegisterModules(); + return objectMapper; + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/config/MessageConfiguration.java b/blade-core-tool/src/main/java/org/springblade/core/tool/config/MessageConfiguration.java new file mode 100644 index 0000000..31d7408 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/config/MessageConfiguration.java @@ -0,0 +1,82 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.tool.config; + + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.AllArgsConstructor; +import org.springblade.core.tool.jackson.BladeJacksonProperties; +import org.springblade.core.tool.jackson.MappingApiJackson2HttpMessageConverter; +import org.springblade.core.tool.utils.DateUtil; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.format.FormatterRegistry; +import org.springframework.format.datetime.DateFormatter; +import org.springframework.http.converter.*; +import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * 消息配置类 + * + * @author Chill + */ +@AutoConfiguration +@AllArgsConstructor +@Order(Ordered.HIGHEST_PRECEDENCE) +public class MessageConfiguration implements WebMvcConfigurer { + + private final ObjectMapper objectMapper; + private final BladeJacksonProperties properties; + + /** + * 使用 JACKSON 作为JSON MessageConverter + * 消息转换,内置断点续传,下载和字符串 + */ + @Override + public void configureMessageConverters(List> converters) { + converters.removeIf(x -> x instanceof StringHttpMessageConverter || x instanceof AbstractJackson2HttpMessageConverter); + converters.add(new StringHttpMessageConverter(StandardCharsets.UTF_8)); + converters.add(new ByteArrayHttpMessageConverter()); + converters.add(new ResourceHttpMessageConverter()); + converters.add(new ResourceRegionHttpMessageConverter()); + converters.add(new MappingApiJackson2HttpMessageConverter(objectMapper, properties)); + } + + /** + * 日期格式化 + */ + @Override + public void addFormatters(FormatterRegistry registry) { + registry.addFormatter(new DateFormatter(DateUtil.PATTERN_DATE)); + registry.addFormatter(new DateFormatter(DateUtil.PATTERN_DATETIME)); + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/config/ToolConfiguration.java b/blade-core-tool/src/main/java/org/springblade/core/tool/config/ToolConfiguration.java new file mode 100644 index 0000000..83de09c --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/config/ToolConfiguration.java @@ -0,0 +1,62 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.tool.config; + + +import org.springblade.core.tool.support.BinderSupplier; +import org.springblade.core.tool.utils.SpringUtil; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; + +import java.util.function.Supplier; + +/** + * 工具配置类 + * + * @author Chill + */ +@AutoConfiguration +public class ToolConfiguration { + + /** + * Spring上下文缓存 + */ + @Bean + public SpringUtil springUtil() { + return new SpringUtil(); + } + + /** + * Binder支持类 + */ + @Bean + @ConditionalOnMissingBean + public Supplier binderSupplier() { + return new BinderSupplier(); + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/constant/BladeConstant.java b/blade-core-tool/src/main/java/org/springblade/core/tool/constant/BladeConstant.java new file mode 100644 index 0000000..de86e87 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/constant/BladeConstant.java @@ -0,0 +1,155 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.tool.constant; + +/** + * 系统常量 + * + * @author Chill + */ +public interface BladeConstant { + + /** + * 编码 + */ + String UTF_8 = "UTF-8"; + + /** + * contentType + */ + String CONTENT_TYPE_NAME = "Content-type"; + + /** + * JSON 资源 + */ + String CONTENT_TYPE = "application/json;charset=utf-8"; + + /** + * 上下文键值 + */ + String CONTEXT_KEY = "bladeContext"; + + /** + * mdc request id key + */ + String MDC_REQUEST_ID_KEY = "requestId"; + + /** + * mdc account id key + */ + String MDC_ACCOUNT_ID_KEY = "accountId"; + + /** + * mdc tenant id key + */ + String MDC_TENANT_ID_KEY = "tenantId"; + + /** + * 角色前缀 + */ + String SECURITY_ROLE_PREFIX = "ROLE_"; + + /** + * 主键字段名 + */ + String DB_PRIMARY_KEY = "id"; + + /** + * 主键字段get方法 + */ + String DB_PRIMARY_KEY_METHOD = "getId"; + + /** + * 租户字段名 + */ + String DB_TENANT_KEY = "tenantId"; + + /** + * 租户字段get方法 + */ + String DB_TENANT_KEY_GET_METHOD = "getTenantId"; + + /** + * 租户字段set方法 + */ + String DB_TENANT_KEY_SET_METHOD = "setTenantId"; + + /** + * 业务状态[1:正常] + */ + int DB_STATUS_NORMAL = 1; + + + /** + * 删除状态[0:正常,1:删除] + */ + int DB_NOT_DELETED = 0; + int DB_IS_DELETED = 1; + + /** + * 用户锁定状态 + */ + int DB_ADMIN_NON_LOCKED = 0; + int DB_ADMIN_LOCKED = 1; + + /** + * 顶级父节点id + */ + Long TOP_PARENT_ID = 0L; + + /** + * 顶级父节点名称 + */ + String TOP_PARENT_NAME = "顶级"; + + /** + * 管理员对应的租户ID + */ + String ADMIN_TENANT_ID = "000000"; + + /** + * 日志默认状态 + */ + String LOG_NORMAL_TYPE = "1"; + + /** + * 默认为空消息 + */ + String DEFAULT_NULL_MESSAGE = "暂无承载数据"; + /** + * 默认成功消息 + */ + String DEFAULT_SUCCESS_MESSAGE = "操作成功"; + /** + * 默认失败消息 + */ + String DEFAULT_FAILURE_MESSAGE = "操作失败"; + /** + * 默认未授权消息 + */ + String DEFAULT_UNAUTHORIZED_MESSAGE = "签名认证失败"; + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/constant/RoleConstant.java b/blade-core-tool/src/main/java/org/springblade/core/tool/constant/RoleConstant.java new file mode 100644 index 0000000..fe1e277 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/constant/RoleConstant.java @@ -0,0 +1,51 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.tool.constant; + +/** + * 系统默认角色 + * + * @author Chill + */ +public class RoleConstant { + + public static final String ADMINISTRATOR = "administrator"; + + public static final String HAS_ROLE_ADMINISTRATOR = "hasRole('" + ADMINISTRATOR + "')"; + + public static final String ADMIN = "admin"; + + public static final String HAS_ROLE_ADMIN = "hasAnyRole('" + ADMINISTRATOR + "', '" + ADMIN + "')"; + + public static final String USER = "user"; + + public static final String HAS_ROLE_USER = "hasRole('" + USER + "')"; + + public static final String TEST = "test"; + + public static final String HAS_ROLE_TEST = "hasRole('" + TEST + "')"; + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/convert/BladeConversionService.java b/blade-core-tool/src/main/java/org/springblade/core/tool/convert/BladeConversionService.java new file mode 100644 index 0000000..f946200 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/convert/BladeConversionService.java @@ -0,0 +1,50 @@ +package org.springblade.core.tool.convert; + +import org.springframework.boot.convert.ApplicationConversionService; +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.lang.Nullable; +import org.springframework.util.StringValueResolver; + +/** + * 类型 转换 服务,添加了 IEnum 转换 + * + * @author L.cm + */ +public class BladeConversionService extends ApplicationConversionService { + @Nullable + private static volatile BladeConversionService SHARED_INSTANCE; + + public BladeConversionService() { + this(null); + } + + public BladeConversionService(@Nullable StringValueResolver embeddedValueResolver) { + super(embeddedValueResolver); + super.addConverter(new EnumToStringConverter()); + super.addConverter(new StringToEnumConverter()); + } + + /** + * Return a shared default application {@code ConversionService} instance, lazily + * building it once needed. + *

+ * Note: This method actually returns an {@link BladeConversionService} + * instance. However, the {@code ConversionService} signature has been preserved for + * binary compatibility. + * @return the shared {@code BladeConversionService} instance (never{@code null}) + */ + public static GenericConversionService getInstance() { + BladeConversionService sharedInstance = BladeConversionService.SHARED_INSTANCE; + if (sharedInstance == null) { + synchronized (BladeConversionService.class) { + sharedInstance = BladeConversionService.SHARED_INSTANCE; + if (sharedInstance == null) { + sharedInstance = new BladeConversionService(); + BladeConversionService.SHARED_INSTANCE = sharedInstance; + } + } + } + return sharedInstance; + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/convert/BladeConverter.java b/blade-core-tool/src/main/java/org/springblade/core/tool/convert/BladeConverter.java new file mode 100644 index 0000000..08ba13f --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/convert/BladeConverter.java @@ -0,0 +1,77 @@ +package org.springblade.core.tool.convert; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.tool.function.CheckedFunction; +import org.springblade.core.tool.utils.ClassUtil; +import org.springblade.core.tool.utils.ConvertUtil; +import org.springblade.core.tool.utils.ReflectUtil; +import org.springblade.core.tool.utils.Unchecked; +import org.springframework.cglib.core.Converter; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.lang.Nullable; + +import java.lang.reflect.Field; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * 组合 spring cglib Converter 和 spring ConversionService + * + * @author L.cm + */ +@Slf4j +@AllArgsConstructor +public class BladeConverter implements Converter { + private static final ConcurrentMap TYPE_CACHE = new ConcurrentHashMap<>(); + private final Class sourceClazz; + private final Class targetClazz; + + /** + * cglib convert + * + * @param value 源对象属性 + * @param target 目标对象属性类 + * @param fieldName 目标的field名,原为 set 方法名,BladeBeanCopier 里做了更改 + * @return {Object} + */ + @Override + @Nullable + public Object convert(Object value, Class target, final Object fieldName) { + if (value == null) { + return null; + } + // 类型一样,不需要转换 + if (ClassUtil.isAssignableValue(target, value)) { + return value; + } + try { + TypeDescriptor targetDescriptor = BladeConverter.getTypeDescriptor(targetClazz, (String) fieldName); + // 1. 判断 sourceClazz 为 Map + if (Map.class.isAssignableFrom(sourceClazz)) { + return ConvertUtil.convert(value, targetDescriptor); + } else { + TypeDescriptor sourceDescriptor = BladeConverter.getTypeDescriptor(sourceClazz, (String) fieldName); + return ConvertUtil.convert(value, sourceDescriptor, targetDescriptor); + } + } catch (Throwable e) { + log.warn("BladeConverter error", e); + return null; + } + } + + private static TypeDescriptor getTypeDescriptor(final Class clazz, final String fieldName) { + String srcCacheKey = clazz.getName() + fieldName; + // 忽略抛出异常的函数,定义完整泛型,避免编译问题 + CheckedFunction uncheckedFunction = (key) -> { + // 这里 property 理论上不会为 null + Field field = ReflectUtil.getField(clazz, fieldName); + if (field == null) { + throw new NoSuchFieldException(fieldName); + } + return new TypeDescriptor(field); + }; + return TYPE_CACHE.computeIfAbsent(srcCacheKey, Unchecked.function(uncheckedFunction)); + } +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/convert/EnumToStringConverter.java b/blade-core-tool/src/main/java/org/springblade/core/tool/convert/EnumToStringConverter.java new file mode 100644 index 0000000..0c1b6b6 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/convert/EnumToStringConverter.java @@ -0,0 +1,135 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.tool.convert; + +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.tool.utils.ConvertUtil; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; +import org.springframework.lang.Nullable; + +import java.lang.reflect.AccessibleObject; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * 接收参数 同 jackson Enum -》 String 转换 + * + * @author L.cm + */ +@Slf4j +public class EnumToStringConverter implements ConditionalGenericConverter { + /** + * 缓存 Enum 类信息,提供性能 + */ + private static final ConcurrentMap, AccessibleObject> ENUM_CACHE_MAP = new ConcurrentHashMap<>(8); + + @Nullable + private static AccessibleObject getAnnotation(Class clazz) { + Set accessibleObjects = new HashSet<>(); + // JsonValue METHOD, FIELD + Field[] fields = clazz.getDeclaredFields(); + Collections.addAll(accessibleObjects, fields); + // methods + Method[] methods = clazz.getDeclaredMethods(); + Collections.addAll(accessibleObjects, methods); + for (AccessibleObject accessibleObject : accessibleObjects) { + // 复用 jackson 的 JsonValue 注解 + JsonValue jsonValue = accessibleObject.getAnnotation(JsonValue.class); + if (jsonValue != null && jsonValue.value()) { + accessibleObject.setAccessible(true); + return accessibleObject; + } + } + return null; + } + + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + return true; + } + + @Override + public Set getConvertibleTypes() { + Set pairSet = new HashSet<>(3); + pairSet.add(new ConvertiblePair(Enum.class, String.class)); + pairSet.add(new ConvertiblePair(Enum.class, Integer.class)); + pairSet.add(new ConvertiblePair(Enum.class, Long.class)); + return Collections.unmodifiableSet(pairSet); + } + + @Override + public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (source == null) { + return null; + } + Class sourceClazz = sourceType.getType(); + AccessibleObject accessibleObject = ENUM_CACHE_MAP.computeIfAbsent(sourceClazz, EnumToStringConverter::getAnnotation); + Class targetClazz = targetType.getType(); + // 如果为null,走默认的转换 + if (accessibleObject == null) { + if (String.class == targetClazz) { + return ((Enum) source).name(); + } + int ordinal = ((Enum) source).ordinal(); + return ConvertUtil.convert(ordinal, targetClazz); + } + try { + return EnumToStringConverter.invoke(sourceClazz, accessibleObject, source, targetClazz); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + return null; + } + + @Nullable + private static Object invoke(Class clazz, AccessibleObject accessibleObject, Object source, Class targetClazz) + throws IllegalAccessException, InvocationTargetException { + Object value = null; + if (accessibleObject instanceof Field) { + Field field = (Field) accessibleObject; + value = field.get(source); + } else if (accessibleObject instanceof Method) { + Method method = (Method) accessibleObject; + Class paramType = method.getParameterTypes()[0]; + // 类型转换 + Object object = ConvertUtil.convert(source, paramType); + value = method.invoke(clazz, object); + } + if (value == null) { + return null; + } + return ConvertUtil.convert(value, targetClazz); + } +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/convert/StringToEnumConverter.java b/blade-core-tool/src/main/java/org/springblade/core/tool/convert/StringToEnumConverter.java new file mode 100644 index 0000000..48b0f0e --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/convert/StringToEnumConverter.java @@ -0,0 +1,135 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.tool.convert; + +import com.fasterxml.jackson.annotation.JsonCreator; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.tool.utils.ConvertUtil; +import org.springblade.core.tool.utils.StringUtil; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; +import org.springframework.lang.Nullable; + +import java.lang.reflect.AccessibleObject; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * 接收参数 同 jackson String -》 Enum 转换 + * + * @author L.cm + */ +@Slf4j +public class StringToEnumConverter implements ConditionalGenericConverter { + /** + * 缓存 Enum 类信息,提供性能 + */ + private static final ConcurrentMap, AccessibleObject> ENUM_CACHE_MAP = new ConcurrentHashMap<>(8); + + @Nullable + private static AccessibleObject getAnnotation(Class clazz) { + Set accessibleObjects = new HashSet<>(); + // JsonCreator METHOD, CONSTRUCTOR + Constructor[] constructors = clazz.getConstructors(); + Collections.addAll(accessibleObjects, constructors); + // methods + Method[] methods = clazz.getDeclaredMethods(); + Collections.addAll(accessibleObjects, methods); + for (AccessibleObject accessibleObject : accessibleObjects) { + // 复用 jackson 的 JsonCreator注解 + JsonCreator jsonCreator = accessibleObject.getAnnotation(JsonCreator.class); + if (jsonCreator != null && JsonCreator.Mode.DISABLED != jsonCreator.mode()) { + accessibleObject.setAccessible(true); + return accessibleObject; + } + } + return null; + } + + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + return true; + } + + @Override + public Set getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(String.class, Enum.class)); + } + + @Nullable + @Override + public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (StringUtil.isBlank((String) source)) { + return null; + } + Class clazz = targetType.getType(); + AccessibleObject accessibleObject = ENUM_CACHE_MAP.computeIfAbsent(clazz, StringToEnumConverter::getAnnotation); + String value = ((String) source).trim(); + // 如果为null,走默认的转换 + if (accessibleObject == null) { + return valueOf(clazz, value); + } + try { + return StringToEnumConverter.invoke(clazz, accessibleObject, value); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + return null; + } + + @SuppressWarnings("unchecked") + private static > T valueOf(Class clazz, String value){ + return Enum.valueOf((Class) clazz, value); + } + + @Nullable + private static Object invoke(Class clazz, AccessibleObject accessibleObject, String value) + throws IllegalAccessException, InvocationTargetException, InstantiationException { + if (accessibleObject instanceof Constructor) { + Constructor constructor = (Constructor) accessibleObject; + Class paramType = constructor.getParameterTypes()[0]; + // 类型转换 + Object object = ConvertUtil.convert(value, paramType); + return constructor.newInstance(object); + } + if (accessibleObject instanceof Method) { + Method method = (Method) accessibleObject; + Class paramType = method.getParameterTypes()[0]; + // 类型转换 + Object object = ConvertUtil.convert(value, paramType); + return method.invoke(clazz, object); + } + return null; + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/function/CheckedCallable.java b/blade-core-tool/src/main/java/org/springblade/core/tool/function/CheckedCallable.java new file mode 100644 index 0000000..d6bfd48 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/function/CheckedCallable.java @@ -0,0 +1,47 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.tool.function; + +import org.springframework.lang.Nullable; + +/** + * 受检的 Callable + * + * @author L.cm + */ +@FunctionalInterface +public interface CheckedCallable { + + /** + * Run this callable. + * + * @return result + * @throws Throwable CheckedException + */ + @Nullable + T call() throws Throwable; +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/function/CheckedComparator.java b/blade-core-tool/src/main/java/org/springblade/core/tool/function/CheckedComparator.java new file mode 100644 index 0000000..7dc83e7 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/function/CheckedComparator.java @@ -0,0 +1,47 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.tool.function; + +/** + * 受检的 Comparator + * + * @author L.cm + */ +@FunctionalInterface +public interface CheckedComparator { + + /** + * Compares its two arguments for order. + * + * @param o1 o1 + * @param o2 o2 + * @return int + * @throws Throwable CheckedException + */ + int compare(T o1, T o2) throws Throwable; + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/function/CheckedConsumer.java b/blade-core-tool/src/main/java/org/springblade/core/tool/function/CheckedConsumer.java new file mode 100644 index 0000000..c7a04f7 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/function/CheckedConsumer.java @@ -0,0 +1,48 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.tool.function; + +import org.springframework.lang.Nullable; + +/** + * 受检的 Consumer + * + * @author L.cm + */ +@FunctionalInterface +public interface CheckedConsumer { + + /** + * Run the Consumer + * + * @param t T + * @throws Throwable UncheckedException + */ + @Nullable + void accept(@Nullable T t) throws Throwable; + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/function/CheckedFunction.java b/blade-core-tool/src/main/java/org/springblade/core/tool/function/CheckedFunction.java new file mode 100644 index 0000000..01235c5 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/function/CheckedFunction.java @@ -0,0 +1,49 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.tool.function; + +import org.springframework.lang.Nullable; + +/** + * 受检的 function + * + * @author L.cm + */ +@FunctionalInterface +public interface CheckedFunction { + + /** + * Run the Function + * + * @param t T + * @return R R + * @throws Throwable CheckedException + */ + @Nullable + R apply(@Nullable T t) throws Throwable; + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/function/CheckedRunnable.java b/blade-core-tool/src/main/java/org/springblade/core/tool/function/CheckedRunnable.java new file mode 100644 index 0000000..07259fc --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/function/CheckedRunnable.java @@ -0,0 +1,44 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.tool.function; + +/** + * 受检的 runnable + * + * @author L.cm + */ +@FunctionalInterface +public interface CheckedRunnable { + + /** + * Run this runnable. + * + * @throws Throwable CheckedException + */ + void run() throws Throwable; + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/function/CheckedSupplier.java b/blade-core-tool/src/main/java/org/springblade/core/tool/function/CheckedSupplier.java new file mode 100644 index 0000000..1b66259 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/function/CheckedSupplier.java @@ -0,0 +1,48 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.tool.function; + +import org.springframework.lang.Nullable; + +/** + * 受检的 Supplier + * + * @author L.cm + */ +@FunctionalInterface +public interface CheckedSupplier { + + /** + * Run the Supplier + * + * @return T + * @throws Throwable CheckedException + */ + @Nullable + T get() throws Throwable; + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/AbstractReadWriteJackson2HttpMessageConverter.java b/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/AbstractReadWriteJackson2HttpMessageConverter.java new file mode 100644 index 0000000..1b2d4f7 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/AbstractReadWriteJackson2HttpMessageConverter.java @@ -0,0 +1,152 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.tool.jackson; + +import com.fasterxml.jackson.core.JsonEncoding; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.PrettyPrinter; +import com.fasterxml.jackson.core.util.DefaultIndenter; +import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.exc.InvalidDefinitionException; +import com.fasterxml.jackson.databind.ser.FilterProvider; +import org.springblade.core.tool.utils.Charsets; +import org.springframework.http.HttpOutputMessage; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConversionException; +import org.springframework.http.converter.HttpMessageNotWritableException; +import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter; +import org.springframework.http.converter.json.MappingJacksonValue; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import org.springframework.util.TypeUtils; + +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +/** + * 分读写的 json 消息 处理器 + * + * @author L.cm + */ +public abstract class AbstractReadWriteJackson2HttpMessageConverter extends AbstractJackson2HttpMessageConverter { + private static final java.nio.charset.Charset DEFAULT_CHARSET = Charsets.UTF_8; + + private final ObjectMapper writeObjectMapper; + @Nullable + private PrettyPrinter ssePrettyPrinter; + + public AbstractReadWriteJackson2HttpMessageConverter(ObjectMapper readObjectMapper, ObjectMapper writeObjectMapper) { + super(readObjectMapper); + this.writeObjectMapper = writeObjectMapper; + initSsePrettyPrinter(); + } + + public AbstractReadWriteJackson2HttpMessageConverter(ObjectMapper readObjectMapper, ObjectMapper writeObjectMapper, MediaType supportedMediaType) { + this(readObjectMapper, writeObjectMapper); + setSupportedMediaTypes(Collections.singletonList(supportedMediaType)); + initSsePrettyPrinter(); + } + + public AbstractReadWriteJackson2HttpMessageConverter(ObjectMapper readObjectMapper, ObjectMapper writeObjectMapper, List supportedMediaTypes) { + this(readObjectMapper, writeObjectMapper); + setSupportedMediaTypes(supportedMediaTypes); + } + + private void initSsePrettyPrinter() { + setDefaultCharset(DEFAULT_CHARSET); + DefaultPrettyPrinter prettyPrinter = new DefaultPrettyPrinter(); + prettyPrinter.indentObjectsWith(new DefaultIndenter(" ", "\ndata:")); + this.ssePrettyPrinter = prettyPrinter; + } + + @Override + public boolean canWrite(@NonNull Class clazz, @Nullable MediaType mediaType) { + if (!canWrite(mediaType)) { + return false; + } + AtomicReference causeRef = new AtomicReference<>(); + if (this.defaultObjectMapper.canSerialize(clazz, causeRef)) { + return true; + } + logWarningIfNecessary(clazz, causeRef.get()); + return false; + } + + @Override + protected void writeInternal(@NonNull Object object, @Nullable Type type, HttpOutputMessage outputMessage) + throws IOException, HttpMessageNotWritableException { + + MediaType contentType = outputMessage.getHeaders().getContentType(); + JsonEncoding encoding = getJsonEncoding(contentType); + JsonGenerator generator = this.writeObjectMapper.getFactory().createGenerator(outputMessage.getBody(), encoding); + try { + writePrefix(generator, object); + + Object value = object; + Class serializationView = null; + FilterProvider filters = null; + JavaType javaType = null; + + if (object instanceof MappingJacksonValue) { + MappingJacksonValue container = (MappingJacksonValue) object; + value = container.getValue(); + serializationView = container.getSerializationView(); + filters = container.getFilters(); + } + if (type != null && TypeUtils.isAssignable(type, value.getClass())) { + javaType = getJavaType(type, null); + } + + ObjectWriter objectWriter = (serializationView != null ? + this.writeObjectMapper.writerWithView(serializationView) : this.writeObjectMapper.writer()); + if (filters != null) { + objectWriter = objectWriter.with(filters); + } + if (javaType != null && javaType.isContainerType()) { + objectWriter = objectWriter.forType(javaType); + } + SerializationConfig config = objectWriter.getConfig(); + if (contentType != null && contentType.isCompatibleWith(MediaType.TEXT_EVENT_STREAM) && + config.isEnabled(SerializationFeature.INDENT_OUTPUT)) { + objectWriter = objectWriter.with(this.ssePrettyPrinter); + } + objectWriter.writeValue(generator, value); + + writeSuffix(generator, object); + generator.flush(); + } catch (InvalidDefinitionException ex) { + throw new HttpMessageConversionException("Type definition error: " + ex.getType(), ex); + } catch (JsonProcessingException ex) { + throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getOriginalMessage(), ex); + } + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/BigNumberSerializer.java b/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/BigNumberSerializer.java new file mode 100644 index 0000000..65fb0cb --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/BigNumberSerializer.java @@ -0,0 +1,69 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.tool.jackson; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JacksonStdImpl; +import com.fasterxml.jackson.databind.ser.std.NumberSerializer; + +import java.io.IOException; + +/** + * 大数值序列化,避免超过js的精度,造成精度丢失 + * + * @author L.cm + */ +@JacksonStdImpl +public class BigNumberSerializer extends NumberSerializer { + + /** + * js 最大值为 Math.pow(2, 53),十进制为:9007199254740992 + */ + private static final long JS_NUM_MAX = 0x20000000000000L; + /** + * js 最小值为 -Math.pow(2, 53),十进制为:-9007199254740992 + */ + private static final long JS_NUM_MIN = -0x20000000000000L; + /** + * Static instance that is only to be used for {@link java.lang.Number}. + */ + public final static BigNumberSerializer instance = new BigNumberSerializer(Number.class); + + public BigNumberSerializer(Class rawType) { + super(rawType); + } + + @Override + public void serialize(Number value, JsonGenerator gen, SerializerProvider provider) throws IOException { + long longValue = value.longValue(); + if (longValue < JS_NUM_MIN || longValue > JS_NUM_MAX) { + gen.writeString(value.toString()); + } else { + super.serialize(value, gen, provider); + } + } +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/BladeBeanSerializerModifier.java b/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/BladeBeanSerializerModifier.java new file mode 100644 index 0000000..e2e4fbb --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/BladeBeanSerializerModifier.java @@ -0,0 +1,132 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.tool.jackson; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.ser.BeanPropertyWriter; +import com.fasterxml.jackson.databind.ser.BeanSerializerModifier; +import org.springblade.core.tool.utils.StringPool; +import org.springblade.core.tool.utils.StringUtil; + +import java.io.IOException; +import java.time.OffsetDateTime; +import java.time.temporal.TemporalAccessor; +import java.util.Collection; +import java.util.Date; +import java.util.List; + +/** + * jackson 默认值为 null 时的处理 + *

+ * 主要是为了避免 app 端出现null导致闪退 + *

+ * 规则: + * number -1 + * string "" + * date "" + * boolean false + * array [] + * Object {} + * + * @author L.cm + */ +public class BladeBeanSerializerModifier extends BeanSerializerModifier { + @Override + public List changeProperties( + SerializationConfig config, BeanDescription beanDesc, + List beanProperties) { + // 循环所有的beanPropertyWriter + beanProperties.forEach(writer -> { + // 如果已经有 null 序列化处理如注解:@JsonSerialize(nullsUsing = xxx) 跳过 + if (writer.hasNullSerializer()) { + return; + } + JavaType type = writer.getType(); + Class clazz = type.getRawClass(); + if (type.isTypeOrSubTypeOf(Number.class)) { + writer.assignNullSerializer(NullJsonSerializers.NUMBER_JSON_SERIALIZER); + } else if (type.isTypeOrSubTypeOf(Boolean.class)) { + writer.assignNullSerializer(NullJsonSerializers.BOOLEAN_JSON_SERIALIZER); + } else if (type.isTypeOrSubTypeOf(Character.class)) { + writer.assignNullSerializer(NullJsonSerializers.STRING_JSON_SERIALIZER); + } else if (type.isTypeOrSubTypeOf(String.class)) { + writer.assignNullSerializer(NullJsonSerializers.STRING_JSON_SERIALIZER); + } else if (type.isArrayType() || clazz.isArray() || type.isTypeOrSubTypeOf(Collection.class)) { + writer.assignNullSerializer(NullJsonSerializers.ARRAY_JSON_SERIALIZER); + } else if (type.isTypeOrSubTypeOf(OffsetDateTime.class)) { + writer.assignNullSerializer(NullJsonSerializers.STRING_JSON_SERIALIZER); + } else if (type.isTypeOrSubTypeOf(Date.class) || type.isTypeOrSubTypeOf(TemporalAccessor.class)) { + writer.assignNullSerializer(NullJsonSerializers.STRING_JSON_SERIALIZER); + } else { + writer.assignNullSerializer(NullJsonSerializers.OBJECT_JSON_SERIALIZER); + } + }); + return super.changeProperties(config, beanDesc, beanProperties); + } + + public interface NullJsonSerializers { + + JsonSerializer STRING_JSON_SERIALIZER = new JsonSerializer() { + @Override + public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeString(StringPool.EMPTY); + } + }; + + JsonSerializer NUMBER_JSON_SERIALIZER = new JsonSerializer() { + @Override + public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeNumber(StringUtil.INDEX_NOT_FOUND); + } + }; + + JsonSerializer BOOLEAN_JSON_SERIALIZER = new JsonSerializer() { + @Override + public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeObject(Boolean.FALSE); + } + }; + + JsonSerializer ARRAY_JSON_SERIALIZER = new JsonSerializer() { + @Override + public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeStartArray(); + gen.writeEndArray(); + } + }; + + JsonSerializer OBJECT_JSON_SERIALIZER = new JsonSerializer() { + @Override + public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeStartObject(); + gen.writeEndObject(); + } + }; + + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/BladeJacksonProperties.java b/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/BladeJacksonProperties.java new file mode 100644 index 0000000..d7e56f7 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/BladeJacksonProperties.java @@ -0,0 +1,55 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.tool.jackson; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * jackson 配置 + * + * @author L.cm + */ +@Getter +@Setter +@ConfigurationProperties("blade.jackson") +public class BladeJacksonProperties { + + /** + * null 转为 空,字符串转成"",数组转为[],对象转为{},数字转为-1 + */ + private Boolean nullToEmpty = Boolean.TRUE; + /** + * 响应到前端,大数值自动写出为 String,避免精度丢失 + */ + private Boolean bigNumToString = Boolean.TRUE; + /** + * 支持 MediaType text/plain,用于和 blade-api-crypto 一起使用 + */ + private Boolean supportTextPlain = Boolean.FALSE; + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/BladeJavaTimeModule.java b/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/BladeJavaTimeModule.java new file mode 100644 index 0000000..04c5557 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/BladeJavaTimeModule.java @@ -0,0 +1,60 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.tool.jackson; + +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.datatype.jsr310.PackageVersion; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer; +import org.springblade.core.tool.utils.DateTimeUtil; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; + +/** + * java 8 时间默认序列化 + * + * @author L.cm + */ +public class BladeJavaTimeModule extends SimpleModule { + public static final BladeJavaTimeModule INSTANCE = new BladeJavaTimeModule(); + + public BladeJavaTimeModule() { + super(PackageVersion.VERSION); + this.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeUtil.DATETIME_FORMAT)); + this.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeUtil.DATE_FORMAT)); + this.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeUtil.TIME_FORMAT)); + this.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeUtil.DATETIME_FORMAT)); + this.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeUtil.DATE_FORMAT)); + this.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeUtil.TIME_FORMAT)); + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/BladeNumberModule.java b/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/BladeNumberModule.java new file mode 100644 index 0000000..2f56e4d --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/BladeNumberModule.java @@ -0,0 +1,57 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.tool.jackson; + +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; + +import java.math.BigDecimal; +import java.math.BigInteger; + +/** + * 大整数序列化为 String 字符串,避免浏览器丢失精度 + * + *

+ * 前端建议采用: + * bignumber 库: https://github.com/MikeMcl/bignumber.js + * decimal.js 库: https://github.com/MikeMcl/decimal.js + *

+ * + * @author L.cm + */ +public class BladeNumberModule extends SimpleModule { + public static final BladeNumberModule INSTANCE = new BladeNumberModule(); + + public BladeNumberModule() { + super(BladeNumberModule.class.getName()); + // Long 和 BigInteger 采用定制的逻辑序列化,避免超过js的精度 + this.addSerializer(Long.class, BigNumberSerializer.instance); + this.addSerializer(Long.TYPE, BigNumberSerializer.instance); + this.addSerializer(BigInteger.class, BigNumberSerializer.instance); + // BigDecimal 采用 toString 避免精度丢失,前端采用 decimal.js 来计算。 + this.addSerializer(BigDecimal.class, ToStringSerializer.instance); + } +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/JsonUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/JsonUtil.java new file mode 100644 index 0000000..011a72a --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/JsonUtil.java @@ -0,0 +1,772 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.tool.jackson; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.TreeNode; +import com.fasterxml.jackson.core.json.JsonReadFeature; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.type.CollectionLikeType; +import com.fasterxml.jackson.databind.type.MapType; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.tool.utils.*; +import org.springframework.lang.Nullable; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.text.SimpleDateFormat; +import java.time.ZoneId; +import java.util.*; + +/** + * Jackson工具类 + * + * @author Chill + */ +@Slf4j +public class JsonUtil { + + /** + * 将对象序列化成json字符串 + * + * @param value javaBean + * @return jsonString json字符串 + */ + public static String toJson(T value) { + try { + return getInstance().writeValueAsString(value); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + return null; + } + + /** + * 将对象序列化成 json byte 数组 + * + * @param object javaBean + * @return jsonString json字符串 + */ + public static byte[] toJsonAsBytes(Object object) { + try { + return getInstance().writeValueAsBytes(object); + } catch (JsonProcessingException e) { + throw Exceptions.unchecked(e); + } + } + + /** + * 将json反序列化成对象 + * + * @param content content + * @param valueType class + * @param T 泛型标记 + * @return Bean + */ + public static T parse(String content, Class valueType) { + try { + return getInstance().readValue(content, valueType); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + return null; + } + + /** + * 将json反序列化成对象 + * + * @param content content + * @param typeReference 泛型类型 + * @param T 泛型标记 + * @return Bean + */ + public static T parse(String content, TypeReference typeReference) { + try { + return getInstance().readValue(content, typeReference); + } catch (IOException e) { + throw Exceptions.unchecked(e); + } + } + + /** + * 将json byte 数组反序列化成对象 + * + * @param bytes json bytes + * @param valueType class + * @param T 泛型标记 + * @return Bean + */ + public static T parse(byte[] bytes, Class valueType) { + try { + return getInstance().readValue(bytes, valueType); + } catch (IOException e) { + throw Exceptions.unchecked(e); + } + } + + + /** + * 将json反序列化成对象 + * + * @param bytes bytes + * @param typeReference 泛型类型 + * @param T 泛型标记 + * @return Bean + */ + public static T parse(byte[] bytes, TypeReference typeReference) { + try { + return getInstance().readValue(bytes, typeReference); + } catch (IOException e) { + throw Exceptions.unchecked(e); + } + } + + /** + * 将json反序列化成对象 + * + * @param in InputStream + * @param valueType class + * @param T 泛型标记 + * @return Bean + */ + public static T parse(InputStream in, Class valueType) { + try { + return getInstance().readValue(in, valueType); + } catch (IOException e) { + throw Exceptions.unchecked(e); + } + } + + /** + * 将json反序列化成对象 + * + * @param in InputStream + * @param typeReference 泛型类型 + * @param T 泛型标记 + * @return Bean + */ + public static T parse(InputStream in, TypeReference typeReference) { + try { + return getInstance().readValue(in, typeReference); + } catch (IOException e) { + throw Exceptions.unchecked(e); + } + } + + /** + * 将json反序列化成List对象 + * + * @param content content + * @param valueTypeRef class + * @param T 泛型标记 + * @return List + */ + public static List parseArray(String content, Class valueTypeRef) { + try { + + if (!StringUtil.startsWithIgnoreCase(content, StringPool.LEFT_SQ_BRACKET)) { + content = StringPool.LEFT_SQ_BRACKET + content + StringPool.RIGHT_SQ_BRACKET; + } + + List> list = getInstance().readValue(content, new TypeReference>>() { + }); + + List result = new ArrayList<>(); + for (Map map : list) { + result.add(toPojo(map, valueTypeRef)); + } + return result; + } catch (IOException e) { + log.error(e.getMessage(), e); + } + return null; + } + + /** + * 将json字符串转成 JsonNode + * + * @param jsonString jsonString + * @return jsonString json字符串 + */ + public static JsonNode readTree(String jsonString) { + Objects.requireNonNull(jsonString, "jsonString is null"); + try { + return getInstance().readTree(jsonString); + } catch (IOException e) { + throw Exceptions.unchecked(e); + } + } + + /** + * 将json字符串转成 JsonNode + * + * @param in InputStream + * @return jsonString json字符串 + */ + public static JsonNode readTree(InputStream in) { + Objects.requireNonNull(in, "InputStream in is null"); + try { + return getInstance().readTree(in); + } catch (IOException e) { + throw Exceptions.unchecked(e); + } + } + + /** + * 将json字符串转成 JsonNode + * + * @param content content + * @return jsonString json字符串 + */ + public static JsonNode readTree(byte[] content) { + Objects.requireNonNull(content, "byte[] content is null"); + try { + return getInstance().readTree(content); + } catch (IOException e) { + throw Exceptions.unchecked(e); + } + } + + /** + * 将json字符串转成 JsonNode + * + * @param jsonParser JsonParser + * @return jsonString json字符串 + */ + public static JsonNode readTree(JsonParser jsonParser) { + Objects.requireNonNull(jsonParser, "jsonParser is null"); + try { + return getInstance().readTree(jsonParser); + } catch (IOException e) { + throw Exceptions.unchecked(e); + } + } + + + /** + * 将json byte 数组反序列化成对象 + * + * @param content json bytes + * @param valueType class + * @param T 泛型标记 + * @return Bean + */ + @Nullable + public static T readValue(@Nullable byte[] content, Class valueType) { + if (ObjectUtil.isEmpty(content)) { + return null; + } + try { + return getInstance().readValue(content, valueType); + } catch (IOException e) { + throw Exceptions.unchecked(e); + } + } + + /** + * 将json反序列化成对象 + * + * @param jsonString jsonString + * @param valueType class + * @param T 泛型标记 + * @return Bean + */ + @Nullable + public static T readValue(@Nullable String jsonString, Class valueType) { + if (StringUtil.isBlank(jsonString)) { + return null; + } + try { + return getInstance().readValue(jsonString, valueType); + } catch (IOException e) { + throw Exceptions.unchecked(e); + } + } + + /** + * 将json反序列化成对象 + * + * @param in InputStream + * @param valueType class + * @param T 泛型标记 + * @return Bean + */ + @Nullable + public static T readValue(@Nullable InputStream in, Class valueType) { + if (in == null) { + return null; + } + try { + return getInstance().readValue(in, valueType); + } catch (IOException e) { + throw Exceptions.unchecked(e); + } + } + + /** + * 将json反序列化成对象 + * + * @param content bytes + * @param typeReference 泛型类型 + * @param T 泛型标记 + * @return Bean + */ + @Nullable + public static T readValue(@Nullable byte[] content, TypeReference typeReference) { + if (ObjectUtil.isEmpty(content)) { + return null; + } + try { + return getInstance().readValue(content, typeReference); + } catch (IOException e) { + throw Exceptions.unchecked(e); + } + } + + /** + * 将json反序列化成对象 + * + * @param jsonString jsonString + * @param typeReference 泛型类型 + * @param T 泛型标记 + * @return Bean + */ + @Nullable + public static T readValue(@Nullable String jsonString, TypeReference typeReference) { + if (StringUtil.isBlank(jsonString)) { + return null; + } + try { + return getInstance().readValue(jsonString, typeReference); + } catch (IOException e) { + throw Exceptions.unchecked(e); + } + } + + /** + * 将json反序列化成对象 + * + * @param in InputStream + * @param typeReference 泛型类型 + * @param T 泛型标记 + * @return Bean + */ + @Nullable + public static T readValue(@Nullable InputStream in, TypeReference typeReference) { + if (in == null) { + return null; + } + try { + return getInstance().readValue(in, typeReference); + } catch (IOException e) { + throw Exceptions.unchecked(e); + } + } + + /** + * 封装 map type + * + * @param keyClass key 类型 + * @param valueClass value 类型 + * @return MapType + */ + public static MapType getMapType(Class keyClass, Class valueClass) { + return getInstance().getTypeFactory().constructMapType(Map.class, keyClass, valueClass); + } + + /** + * 封装 map type + * + * @param elementClass 集合值类型 + * @return CollectionLikeType + */ + public static CollectionLikeType getListType(Class elementClass) { + return getInstance().getTypeFactory().constructCollectionLikeType(List.class, elementClass); + } + + /** + * 读取集合 + * + * @param content bytes + * @param elementClass elementClass + * @param 泛型 + * @return 集合 + */ + public static List readList(@Nullable byte[] content, Class elementClass) { + if (ObjectUtil.isEmpty(content)) { + return Collections.emptyList(); + } + try { + return getInstance().readValue(content, getListType(elementClass)); + } catch (IOException e) { + throw Exceptions.unchecked(e); + } + } + + /** + * 读取集合 + * + * @param content InputStream + * @param elementClass elementClass + * @param 泛型 + * @return 集合 + */ + public static List readList(@Nullable InputStream content, Class elementClass) { + if (content == null) { + return Collections.emptyList(); + } + try { + return getInstance().readValue(content, getListType(elementClass)); + } catch (IOException e) { + throw Exceptions.unchecked(e); + } + } + + /** + * 读取集合 + * + * @param content bytes + * @param elementClass elementClass + * @param 泛型 + * @return 集合 + */ + public static List readList(@Nullable String content, Class elementClass) { + if (ObjectUtil.isEmpty(content)) { + return Collections.emptyList(); + } + try { + return getInstance().readValue(content, getListType(elementClass)); + } catch (IOException e) { + throw Exceptions.unchecked(e); + } + } + + /** + * 读取集合 + * + * @param content InputStream + * @param valueClass 值类型 + * @param 泛型 + * @return 集合 + */ + public static Map readMap(@Nullable InputStream content, Class valueClass) { + return readMap(content, String.class, valueClass); + } + + /** + * 读取集合 + * + * @param reader java.io.Reader + * @param valueClass 值类型 + * @param 泛型 + * @return 集合 + */ + public static Map readMap(@Nullable Reader reader, Class valueClass) { + return readMap(reader, String.class, valueClass); + } + + /** + * 读取集合 + * + * @param content bytes + * @param valueClass 值类型 + * @param 泛型 + * @return 集合 + */ + public static Map readMap(@Nullable String content, Class valueClass) { + return readMap(content, String.class, valueClass); + } + + /** + * 读取集合 + * + * @param content bytes + * @param keyClass key类型 + * @param valueClass 值类型 + * @param 泛型 + * @param 泛型 + * @return 集合 + */ + public static Map readMap(@Nullable byte[] content, Class keyClass, Class valueClass) { + if (ObjectUtil.isEmpty(content)) { + return Collections.emptyMap(); + } + try { + return getInstance().readValue(content, getMapType(keyClass, valueClass)); + } catch (IOException e) { + throw Exceptions.unchecked(e); + } + } + + /** + * 读取集合 + * + * @param content InputStream + * @param keyClass key类型 + * @param valueClass 值类型 + * @param 泛型 + * @param 泛型 + * @return 集合 + */ + public static Map readMap(@Nullable InputStream content, Class keyClass, Class valueClass) { + if (ObjectUtil.isEmpty(content)) { + return Collections.emptyMap(); + } + try { + return getInstance().readValue(content, getMapType(keyClass, valueClass)); + } catch (IOException e) { + throw Exceptions.unchecked(e); + } + } + + /** + * 读取集合 + * + * @param reader java.io.Reader + * @param keyClass key类型 + * @param valueClass 值类型 + * @param 泛型 + * @param 泛型 + * @return 集合 + */ + public static Map readMap(@Nullable Reader reader, Class keyClass, Class valueClass) { + if (reader == null) { + return Collections.emptyMap(); + } + try { + return getInstance().readValue(reader, getMapType(keyClass, valueClass)); + } catch (IOException e) { + throw Exceptions.unchecked(e); + } + } + + /** + * 读取集合 + * + * @param content bytes + * @param keyClass key类型 + * @param valueClass 值类型 + * @param 泛型 + * @param 泛型 + * @return 集合 + */ + public static Map readMap(@Nullable String content, Class keyClass, Class valueClass) { + if (ObjectUtil.isEmpty(content)) { + return Collections.emptyMap(); + } + try { + return getInstance().readValue(content, getMapType(keyClass, valueClass)); + } catch (IOException e) { + throw Exceptions.unchecked(e); + } + } + + /** + * 读取集合 + * + * @param content bytes + * @return 集合 + */ + public static Map readMap(@Nullable String content) { + return readMap(content, String.class, Object.class); + } + + /** + * 读取集合 + * + * @param content bytes + * @return 集合 + */ + public static List> readListMap(@Nullable String content) { + if (ObjectUtil.isEmpty(content)) { + return Collections.emptyList(); + } + try { + return getInstance().readValue(content, new TypeReference>>() { + }); + } catch (IOException e) { + throw Exceptions.unchecked(e); + } + } + + /** + * jackson 的类型转换 + * + * @param fromValue 来源对象 + * @param toValueType 转换的类型 + * @param 泛型标记 + * @return 转换结果 + */ + public static T convertValue(Object fromValue, Class toValueType) { + return getInstance().convertValue(fromValue, toValueType); + } + + /** + * jackson 的类型转换 + * + * @param fromValue 来源对象 + * @param toValueType 转换的类型 + * @param 泛型标记 + * @return 转换结果 + */ + public static T convertValue(Object fromValue, JavaType toValueType) { + return getInstance().convertValue(fromValue, toValueType); + } + + /** + * jackson 的类型转换 + * + * @param fromValue 来源对象 + * @param toValueTypeRef 泛型类型 + * @param 泛型标记 + * @return 转换结果 + */ + public static T convertValue(Object fromValue, TypeReference toValueTypeRef) { + return getInstance().convertValue(fromValue, toValueTypeRef); + } + + /** + * tree 转对象 + * + * @param treeNode TreeNode + * @param valueType valueType + * @param 泛型标记 + * @return 转换结果 + */ + public static T treeToValue(TreeNode treeNode, Class valueType) { + try { + return getInstance().treeToValue(treeNode, valueType); + } catch (JsonProcessingException e) { + throw Exceptions.unchecked(e); + } + } + + /** + * 对象转为 json node + * + * @param value 对象 + * @return JsonNode + */ + public static JsonNode valueToTree(@Nullable Object value) { + return getInstance().valueToTree(value); + } + + /** + * 判断是否可以序列化 + * + * @param value 对象 + * @return 是否可以序列化 + */ + public static boolean canSerialize(@Nullable Object value) { + if (value == null) { + return true; + } + return getInstance().canSerialize(value.getClass()); + } + + public static Map toMap(String content) { + try { + return getInstance().readValue(content, Map.class); + } catch (IOException e) { + log.error(e.getMessage(), e); + } + return null; + } + + public static Map toMap(String content, Class valueTypeRef) { + try { + Map> map = getInstance().readValue(content, new TypeReference>>() { + }); + Map result = new HashMap<>(16); + for (Map.Entry> entry : map.entrySet()) { + result.put(entry.getKey(), toPojo(entry.getValue(), valueTypeRef)); + } + return result; + } catch (IOException e) { + log.error(e.getMessage(), e); + } + return null; + } + + public static T toPojo(Map fromValue, Class toValueType) { + return getInstance().convertValue(fromValue, toValueType); + } + + public static ObjectMapper getInstance() { + return JacksonHolder.INSTANCE; + } + + private static class JacksonHolder { + private static final ObjectMapper INSTANCE = new JacksonObjectMapper(); + } + + private static class JacksonObjectMapper extends ObjectMapper { + private static final long serialVersionUID = 4288193147502386170L; + + private static final Locale CHINA = Locale.CHINA; + + public JacksonObjectMapper(ObjectMapper src) { + super(src); + } + + public JacksonObjectMapper() { + super(); + //设置地点为中国 + super.setLocale(CHINA); + //去掉默认的时间戳格式 + super.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); + //设置为中国上海时区 + super.setTimeZone(TimeZone.getTimeZone(ZoneId.systemDefault())); + //序列化时,日期的统一格式 + super.setDateFormat(new SimpleDateFormat(DateUtil.PATTERN_DATETIME, Locale.CHINA)); + // 单引号 + super.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true); + // 允许JSON字符串包含非引号控制字符(值小于32的ASCII字符,包含制表符和换行符) + super.configure(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature(), true); + super.configure(JsonReadFeature.ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER.mappedFeature(), true); + super.findAndRegisterModules(); + //失败处理 + super.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); + super.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + //单引号处理 + super.configure(JsonReadFeature.ALLOW_SINGLE_QUOTES.mappedFeature(), true); + //反序列化时,属性不存在的兼容处理s + super.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + //日期格式化 + super.registerModule(new BladeJavaTimeModule()); + super.findAndRegisterModules(); + } + + @Override + public ObjectMapper copy() { + return new JacksonObjectMapper(this); + } + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/MappingApiJackson2HttpMessageConverter.java b/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/MappingApiJackson2HttpMessageConverter.java new file mode 100644 index 0000000..86df677 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/MappingApiJackson2HttpMessageConverter.java @@ -0,0 +1,133 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.tool.jackson; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springblade.core.tool.utils.Charsets; +import org.springframework.http.HttpOutputMessage; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageNotWritableException; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import org.springframework.util.StreamUtils; + +import java.io.IOException; +import java.lang.reflect.Type; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; + +/** + * 针对 api 服务对 android 和 ios 处理的 分读写的 jackson 处理 + * + *

+ * 1. app 端上报数据是 使用 readObjectMapper + * 2. 返回给 app 端的数据使用 writeObjectMapper + * 3. 如果是返回字符串,直接相应,不做 json 处理 + *

+ * + * @author L.cm + */ +public class MappingApiJackson2HttpMessageConverter extends AbstractReadWriteJackson2HttpMessageConverter { + + @Nullable + private String jsonPrefix; + + public MappingApiJackson2HttpMessageConverter(ObjectMapper objectMapper, BladeJacksonProperties properties) { + super(objectMapper, initWriteObjectMapper(objectMapper, properties), initMediaType(properties)); + } + + private static List initMediaType(BladeJacksonProperties properties) { + List supportedMediaTypes = new ArrayList<>(); + supportedMediaTypes.add(MediaType.APPLICATION_JSON); + supportedMediaTypes.add(new MediaType("application", "*+json")); + // 支持 text 文本,用于报文签名 + if (Boolean.TRUE.equals(properties.getSupportTextPlain())) { + supportedMediaTypes.add(MediaType.TEXT_PLAIN); + } + return supportedMediaTypes; + } + + private static ObjectMapper initWriteObjectMapper(ObjectMapper readObjectMapper, BladeJacksonProperties properties) { + // 拷贝 readObjectMapper + ObjectMapper writeObjectMapper = readObjectMapper.copy(); + // 大数字 转 字符串 + if (Boolean.TRUE.equals(properties.getBigNumToString())) { + writeObjectMapper.registerModules(BladeNumberModule.INSTANCE); + } + // null 处理 + if (Boolean.TRUE.equals(properties.getNullToEmpty())) { + writeObjectMapper.setSerializerFactory(writeObjectMapper.getSerializerFactory().withSerializerModifier(new BladeBeanSerializerModifier())); + writeObjectMapper.getSerializerProvider().setNullValueSerializer(BladeBeanSerializerModifier.NullJsonSerializers.STRING_JSON_SERIALIZER); + } + return writeObjectMapper; + } + + @Override + protected void writeInternal(@NonNull Object object, @Nullable Type type, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { + // 如果是字符串,直接写出 + if (object instanceof String) { + Charset defaultCharset = this.getDefaultCharset(); + Charset charset = defaultCharset == null ? Charsets.UTF_8 : defaultCharset; + StreamUtils.copy((String) object, charset, outputMessage.getBody()); + } else { + super.writeInternal(object, type, outputMessage); + } + } + + /** + * Specify a custom prefix to use for this view's JSON output. + * Default is none. + * + * @param jsonPrefix jsonPrefix + * @see #setPrefixJson + */ + public void setJsonPrefix(@Nullable String jsonPrefix) { + this.jsonPrefix = jsonPrefix; + } + + /** + * Indicate whether the JSON output by this view should be prefixed with ")]}', ". Default is false. + *

Prefixing the JSON string in this manner is used to help prevent JSON Hijacking. + * The prefix renders the string syntactically invalid as a script so that it cannot be hijacked. + * This prefix should be stripped before parsing the string as JSON. + * + * @param prefixJson prefixJson + * @see #setJsonPrefix + */ + public void setPrefixJson(boolean prefixJson) { + this.jsonPrefix = (prefixJson ? ")]}', " : null); + } + + @Override + protected void writePrefix(@NonNull JsonGenerator generator, @NonNull Object object) throws IOException { + if (this.jsonPrefix != null) { + generator.writeRaw(this.jsonPrefix); + } + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/node/BaseNode.java b/blade-core-tool/src/main/java/org/springblade/core/tool/node/BaseNode.java new file mode 100644 index 0000000..9044ab1 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/node/BaseNode.java @@ -0,0 +1,86 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.tool.node; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import lombok.Data; + +import java.io.Serial; +import java.util.ArrayList; +import java.util.List; + +/** + * 节点基类 + * + * @author smallchill + */ +@Data +public class BaseNode implements INode { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 主键ID + */ + @JsonSerialize(using = ToStringSerializer.class) + protected Long id; + + /** + * 父节点ID + */ + @JsonSerialize(using = ToStringSerializer.class) + protected Long parentId; + + /** + * 子孙节点 + */ + @JsonInclude(JsonInclude.Include.NON_EMPTY) + protected List children = new ArrayList(); + + /** + * 是否有子孙节点 + */ + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private Boolean hasChildren; + + /** + * 是否有子孙节点 + * + * @return Boolean + */ + @Override + public Boolean getHasChildren() { + if (children.size() > 0) { + return true; + } else { + return this.hasChildren; + } + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/node/ForestNode.java b/blade-core-tool/src/main/java/org/springblade/core/tool/node/ForestNode.java new file mode 100644 index 0000000..6e66005 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/node/ForestNode.java @@ -0,0 +1,57 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.tool.node; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serial; + + +/** + * 森林节点类 + * + * @author smallchill + */ +@Data +@EqualsAndHashCode(callSuper = false) +public class ForestNode extends BaseNode { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 节点内容 + */ + private Object content; + + public ForestNode(Long id, Long parentId, Object content) { + this.id = id; + this.parentId = parentId; + this.content = content; + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/node/ForestNodeManager.java b/blade-core-tool/src/main/java/org/springblade/core/tool/node/ForestNodeManager.java new file mode 100644 index 0000000..104f727 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/node/ForestNodeManager.java @@ -0,0 +1,94 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.tool.node; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; +import org.springblade.core.tool.utils.StringPool; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * 森林管理类 + * + * @author smallchill + */ +public class ForestNodeManager> { + + /** + * 森林的所有节点 + */ + private final ImmutableMap nodeMap; + + /** + * 森林的父节点ID + */ + private final Map parentIdMap = Maps.newHashMap(); + + public ForestNodeManager(List nodes) { + nodeMap = Maps.uniqueIndex(nodes, INode::getId); + } + + /** + * 根据节点ID获取一个节点 + * + * @param id 节点ID + * @return 对应的节点对象 + */ + public INode getTreeNodeAt(Long id) { + if (nodeMap.containsKey(id)) { + return nodeMap.get(id); + } + return null; + } + + /** + * 增加父节点ID + * + * @param parentId 父节点ID + */ + public void addParentId(Long parentId) { + parentIdMap.put(parentId, StringPool.EMPTY); + } + + /** + * 获取树的根节点(一个森林对应多颗树) + * + * @return 树的根节点集合 + */ + public List getRoot() { + List roots = new ArrayList<>(); + nodeMap.forEach((key, node) -> { + if (node.getParentId() == 0 || parentIdMap.containsKey(node.getId())) { + roots.add(node); + } + }); + return roots; + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/node/ForestNodeMerger.java b/blade-core-tool/src/main/java/org/springblade/core/tool/node/ForestNodeMerger.java new file mode 100644 index 0000000..988edec --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/node/ForestNodeMerger.java @@ -0,0 +1,59 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.tool.node; + +import java.util.List; + +/** + * 森林节点归并类 + * + * @author smallchill + */ +public class ForestNodeMerger { + + /** + * 将节点数组归并为一个森林(多棵树)(填充节点的children域) + * 时间复杂度为O(n^2) + * + * @param items 节点域 + * @return 多棵树的根节点集合 + */ + public static > List merge(List items) { + ForestNodeManager forestNodeManager = new ForestNodeManager<>(items); + items.forEach(forestNode -> { + if (forestNode.getParentId() != 0) { + INode node = forestNodeManager.getTreeNodeAt(forestNode.getParentId()); + if (node != null) { + node.getChildren().add(forestNode); + } else { + forestNodeManager.addParentId(forestNode.getId()); + } + } + }); + return forestNodeManager.getRoot(); + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/node/INode.java b/blade-core-tool/src/main/java/org/springblade/core/tool/node/INode.java new file mode 100644 index 0000000..9433910 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/node/INode.java @@ -0,0 +1,68 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.tool.node; + +import java.io.Serializable; +import java.util.List; + +/** + * Created by Blade. + * + * @author smallchill + */ +public interface INode extends Serializable { + + /** + * 主键 + * + * @return Long + */ + Long getId(); + + /** + * 父主键 + * + * @return Long + */ + Long getParentId(); + + /** + * 子孙节点 + * + * @return List + */ + List getChildren(); + + /** + * 是否有子孙节点 + * + * @return Boolean + */ + default Boolean getHasChildren() { + return false; + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/node/NodeTest.java b/blade-core-tool/src/main/java/org/springblade/core/tool/node/NodeTest.java new file mode 100644 index 0000000..8c98b5e --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/node/NodeTest.java @@ -0,0 +1,33 @@ +package org.springblade.core.tool.node; + +import org.springblade.core.tool.jackson.JsonUtil; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Blade. + * + * @author smallchill + */ +public class NodeTest { + + public static void main(String[] args) { + List list = new ArrayList<>(); + list.add(new ForestNode(1L, 0L, "1")); + list.add(new ForestNode(2L, 0L, "2")); + list.add(new ForestNode(3L, 1L, "3")); + list.add(new ForestNode(4L, 2L, "4")); + list.add(new ForestNode(5L, 3L, "5")); + list.add(new ForestNode(6L, 4L, "6")); + list.add(new ForestNode(7L, 3L, "7")); + list.add(new ForestNode(8L, 5L, "8")); + list.add(new ForestNode(9L, 6L, "9")); + list.add(new ForestNode(10L, 9L, "10")); + List tns = ForestNodeMerger.merge(list); + tns.forEach(node -> + System.out.println(JsonUtil.toJson(node)) + ); + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/node/TreeNode.java b/blade-core-tool/src/main/java/org/springblade/core/tool/node/TreeNode.java new file mode 100644 index 0000000..114aa0c --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/node/TreeNode.java @@ -0,0 +1,72 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.tool.node; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import lombok.Data; +import org.springblade.core.tool.utils.Func; + +import java.io.Serial; +import java.util.Objects; + +/** + * 树型节点类 + * + * @author smallchill + */ +@Data +public class TreeNode extends BaseNode { + + @Serial + private static final long serialVersionUID = 1L; + + private String title; + + @JsonSerialize(using = ToStringSerializer.class) + private Long key; + + @JsonSerialize(using = ToStringSerializer.class) + private Long value; + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + TreeNode other = (TreeNode) obj; + return Func.equals(this.getId(), other.getId()); + } + + @Override + public int hashCode() { + return Objects.hash(id, parentId); + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/spel/BladeExpressionEvaluator.java b/blade-core-tool/src/main/java/org/springblade/core/tool/spel/BladeExpressionEvaluator.java new file mode 100644 index 0000000..74f68e7 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/spel/BladeExpressionEvaluator.java @@ -0,0 +1,120 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.tool.spel; + +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.context.expression.AnnotatedElementKey; +import org.springframework.context.expression.BeanFactoryResolver; +import org.springframework.context.expression.CachedExpressionEvaluator; +import org.springframework.context.expression.MethodBasedEvaluationContext; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.lang.Nullable; + +import java.lang.reflect.Method; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 缓存 spEl 提高性能 + * + * @author L.cm + */ +public class BladeExpressionEvaluator extends CachedExpressionEvaluator { + private final Map expressionCache = new ConcurrentHashMap<>(64); + private final Map methodCache = new ConcurrentHashMap<>(64); + + /** + * Create an {@link EvaluationContext}. + * + * @param method the method + * @param args the method arguments + * @param target the target object + * @param targetClass the target class + * @return the evaluation context + */ + public EvaluationContext createContext(Method method, Object[] args, Object target, Class targetClass, @Nullable BeanFactory beanFactory) { + Method targetMethod = getTargetMethod(targetClass, method); + BladeExpressionRootObject rootObject = new BladeExpressionRootObject(method, args, target, targetClass, targetMethod); + MethodBasedEvaluationContext evaluationContext = new MethodBasedEvaluationContext(rootObject, targetMethod, args, getParameterNameDiscoverer()); + if (beanFactory != null) { + evaluationContext.setBeanResolver(new BeanFactoryResolver(beanFactory)); + } + return evaluationContext; + } + + /** + * Create an {@link EvaluationContext}. + * + * @param method the method + * @param args the method arguments + * @param rootObject rootObject + * @param targetClass the target class + * @return the evaluation context + */ + public EvaluationContext createContext(Method method, Object[] args, Class targetClass, Object rootObject, @Nullable BeanFactory beanFactory) { + Method targetMethod = getTargetMethod(targetClass, method); + MethodBasedEvaluationContext evaluationContext = new MethodBasedEvaluationContext(rootObject, targetMethod, args, getParameterNameDiscoverer()); + if (beanFactory != null) { + evaluationContext.setBeanResolver(new BeanFactoryResolver(beanFactory)); + } + return evaluationContext; + } + + @Nullable + public Object eval(String expression, AnnotatedElementKey methodKey, EvaluationContext evalContext) { + return eval(expression, methodKey, evalContext, null); + } + + @Nullable + public T eval(String expression, AnnotatedElementKey methodKey, EvaluationContext evalContext, @Nullable Class valueType) { + return getExpression(this.expressionCache, methodKey, expression).getValue(evalContext, valueType); + } + + @Nullable + public String evalAsText(String expression, AnnotatedElementKey methodKey, EvaluationContext evalContext) { + return eval(expression, methodKey, evalContext, String.class); + } + + public boolean evalAsBool(String expression, AnnotatedElementKey methodKey, EvaluationContext evalContext) { + return Boolean.TRUE.equals(eval(expression, methodKey, evalContext, Boolean.class)); + } + + private Method getTargetMethod(Class targetClass, Method method) { + AnnotatedElementKey methodKey = new AnnotatedElementKey(method, targetClass); + return methodCache.computeIfAbsent(methodKey, (key) -> AopUtils.getMostSpecificMethod(method, targetClass)); + } + + /** + * Clear all caches. + */ + public void clear() { + this.expressionCache.clear(); + this.methodCache.clear(); + } +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/spel/BladeExpressionRootObject.java b/blade-core-tool/src/main/java/org/springblade/core/tool/spel/BladeExpressionRootObject.java new file mode 100644 index 0000000..903266e --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/spel/BladeExpressionRootObject.java @@ -0,0 +1,51 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.tool.spel; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.lang.reflect.Method; + +/** + * ExpressionRootObject + * + * @author L.cm + */ +@Getter +@AllArgsConstructor +public class BladeExpressionRootObject { + private final Method method; + + private final Object[] args; + + private final Object target; + + private final Class targetClass; + + private final Method targetMethod; +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/ssl/DisableValidationTrustManager.java b/blade-core-tool/src/main/java/org/springblade/core/tool/ssl/DisableValidationTrustManager.java new file mode 100644 index 0000000..2fa209f --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/ssl/DisableValidationTrustManager.java @@ -0,0 +1,55 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.tool.ssl; + +import javax.net.ssl.X509TrustManager; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +/** + * 不进行证书校验 + * + * @author L.cm + */ +public class DisableValidationTrustManager implements X509TrustManager { + + public static final X509TrustManager INSTANCE = new DisableValidationTrustManager(); + + @Override + public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { + } + + @Override + public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/ssl/TrustAllHostNames.java b/blade-core-tool/src/main/java/org/springblade/core/tool/ssl/TrustAllHostNames.java new file mode 100644 index 0000000..cc34ace --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/ssl/TrustAllHostNames.java @@ -0,0 +1,44 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.tool.ssl; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLSession; + +/** + * 信任所有 host name + * + * @author L.cm + */ +public class TrustAllHostNames implements HostnameVerifier { + public static final TrustAllHostNames INSTANCE = new TrustAllHostNames(); + + @Override + public boolean verify(String s, SSLSession sslSession) { + return true; + } +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/support/BeanDiff.java b/blade-core-tool/src/main/java/org/springblade/core/tool/support/BeanDiff.java new file mode 100644 index 0000000..c4354cc --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/support/BeanDiff.java @@ -0,0 +1,31 @@ +package org.springblade.core.tool.support; + +import lombok.Getter; +import lombok.ToString; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * 跟踪类变动比较 + * + * @author L.cm + */ +@Getter +@ToString +public class BeanDiff { + /** + * 变更字段 + */ + private final Set fields = new HashSet<>(); + /** + * 旧值 + */ + private final Map oldValues = new HashMap<>(); + /** + * 新值 + */ + private final Map newValues = new HashMap<>(); +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/support/BinderSupplier.java b/blade-core-tool/src/main/java/org/springblade/core/tool/support/BinderSupplier.java new file mode 100644 index 0000000..b0f295f --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/support/BinderSupplier.java @@ -0,0 +1,41 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.tool.support; + +import java.util.function.Supplier; + +/** + * 解决 no binder available 问题 + * + * @author Chill + */ +public class BinderSupplier implements Supplier { + + @Override + public Object get() { + return null; + } +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/support/CoreMain.java b/blade-core-tool/src/main/java/org/springblade/core/tool/support/CoreMain.java new file mode 100644 index 0000000..6cec26e --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/support/CoreMain.java @@ -0,0 +1,39 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.tool.support; + +/** + * Created by Blade. + * + * @author Chill + */ +public class CoreMain { + + public static void main(String[] args) { + System.out.println("init core module"); + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/support/FastStringWriter.java b/blade-core-tool/src/main/java/org/springblade/core/tool/support/FastStringWriter.java new file mode 100644 index 0000000..a6aed58 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/support/FastStringWriter.java @@ -0,0 +1,264 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.tool.support; + + +import org.springblade.core.tool.utils.StringPool; + +import java.io.IOException; +import java.io.Writer; +import java.util.Arrays; + +/** + * FastStringWriter,更改于 jdk CharArrayWriter + * + *

+ * 1. 去掉了锁 + * 2. 初始容量由 32 改为 64 + *

+ * + * @author L.cm + */ +public class FastStringWriter extends Writer { + /** + * The buffer where data is stored. + */ + private char[] buf; + /** + * The number of chars in the buffer. + */ + private int count; + + /** + * Creates a new CharArrayWriter. + */ + public FastStringWriter() { + this(64); + } + + /** + * Creates a new CharArrayWriter with the specified initial size. + * + * @param initialSize an int specifying the initial buffer size. + * @throws IllegalArgumentException if initialSize is negative + */ + public FastStringWriter(int initialSize) { + if (initialSize < 0) { + throw new IllegalArgumentException("Negative initial size: " + initialSize); + } + buf = new char[initialSize]; + } + + /** + * Writes a character to the buffer. + */ + @Override + public void write(int c) { + int newCount = count + 1; + if (newCount > buf.length) { + buf = Arrays.copyOf(buf, Math.max(buf.length << 1, newCount)); + } + buf[count] = (char) c; + count = newCount; + } + + /** + * Writes characters to the buffer. + * + * @param c the data to be written + * @param off the start offset in the data + * @param len the number of chars that are written + */ + @Override + public void write(char[] c, int off, int len) { + if ((off < 0) || (off > c.length) || (len < 0) || + ((off + len) > c.length) || ((off + len) < 0)) { + throw new IndexOutOfBoundsException(); + } else if (len == 0) { + return; + } + int newCount = count + len; + if (newCount > buf.length) { + buf = Arrays.copyOf(buf, Math.max(buf.length << 1, newCount)); + } + System.arraycopy(c, off, buf, count, len); + count = newCount; + } + + /** + * Write a portion of a string to the buffer. + * + * @param str String to be written from + * @param off Offset from which to start reading characters + * @param len Number of characters to be written + */ + @Override + public void write(String str, int off, int len) { + int newCount = count + len; + if (newCount > buf.length) { + buf = Arrays.copyOf(buf, Math.max(buf.length << 1, newCount)); + } + str.getChars(off, off + len, buf, count); + count = newCount; + } + + /** + * Writes the contents of the buffer to another character stream. + * + * @param out the output stream to write to + * @throws IOException If an I/O error occurs. + */ + public void writeTo(Writer out) throws IOException { + out.write(buf, 0, count); + } + + /** + * Appends the specified character sequence to this writer. + * + *

An invocation of this method of the form out.append(csq) + * behaves in exactly the same way as the invocation + * + *

+	 *     out.write(csq.toString()) 
+ * + *

Depending on the specification of toString for the + * character sequence csq, the entire sequence may not be + * appended. For instance, invoking the toString method of a + * character buffer will return a subsequence whose content depends upon + * the buffer's position and limit. + * + * @param csq The character sequence to append. If csq is + * null, then the four characters "null" are + * appended to this writer. + * @return This writer + */ + @Override + public FastStringWriter append(CharSequence csq) { + String s = (csq == null ? StringPool.NULL : csq.toString()); + write(s, 0, s.length()); + return this; + } + + /** + * Appends a subsequence of the specified character sequence to this writer. + * + *

An invocation of this method of the form out.append(csq, start, + * end) when csq is not null, behaves in + * exactly the same way as the invocation + * + *

+	 *     out.write(csq.subSequence(start, end).toString()) 
+ * + * @param csq The character sequence from which a subsequence will be + * appended. If csq is null, then characters + * will be appended as if csq contained the four + * characters "null". + * @param start The index of the first character in the subsequence + * @param end The index of the character following the last character in the + * subsequence + * @return This writer + * @throws IndexOutOfBoundsException If start or end are negative, start + * is greater than end, or end is greater than + * csq.length() + */ + @Override + public FastStringWriter append(CharSequence csq, int start, int end) { + String s = (csq == null ? StringPool.NULL : csq).subSequence(start, end).toString(); + write(s, 0, s.length()); + return this; + } + + /** + * Appends the specified character to this writer. + * + *

An invocation of this method of the form out.append(c) + * behaves in exactly the same way as the invocation + * + *

+	 *     out.write(c) 
+ * + * @param c The 16-bit character to append + * @return This writer + */ + @Override + public FastStringWriter append(char c) { + write(c); + return this; + } + + /** + * Resets the buffer so that you can use it again without + * throwing away the already allocated buffer. + */ + public void reset() { + count = 0; + } + + /** + * Returns a copy of the input data. + * + * @return an array of chars copied from the input data. + */ + public char[] toCharArray() { + return Arrays.copyOf(buf, count); + } + + /** + * Returns the current size of the buffer. + * + * @return an int representing the current size of the buffer. + */ + public int size() { + return count; + } + + /** + * Converts input data to a string. + * + * @return the string. + */ + @Override + public String toString() { + return new String(buf, 0, count); + } + + /** + * Flush the stream. + */ + @Override + public void flush() { + } + + /** + * Close the stream. This method does not release the buffer, since its + * contents might still be required. Note: Invoking this method in this class + * will have no effect. + */ + @Override + public void close() { + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/support/IMultiOutputStream.java b/blade-core-tool/src/main/java/org/springblade/core/tool/support/IMultiOutputStream.java new file mode 100644 index 0000000..2f90834 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/support/IMultiOutputStream.java @@ -0,0 +1,45 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.tool.support; + +import java.io.OutputStream; + +/** + * A factory for creating MultiOutputStream objects. + * + * @author Chill + */ +public interface IMultiOutputStream { + + /** + * Builds the output stream. + * + * @param params the params + * @return the output stream + */ + OutputStream buildOutputStream(Integer... params); + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/support/ImagePosition.java b/blade-core-tool/src/main/java/org/springblade/core/tool/support/ImagePosition.java new file mode 100644 index 0000000..e72b196 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/support/ImagePosition.java @@ -0,0 +1,159 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.tool.support; + +/** + * 图片操作类 + * + * @author Chill + */ +public class ImagePosition { + + /** + * 图片顶部. + */ + public static final int TOP = 32; + + /** + * 图片中部. + */ + public static final int MIDDLE = 16; + + /** + * 图片底部. + */ + public static final int BOTTOM = 8; + + /** + * 图片左侧. + */ + public static final int LEFT = 4; + + /** + * 图片居中. + */ + public static final int CENTER = 2; + + /** + * 图片右侧. + */ + public static final int RIGHT = 1; + + /** + * 横向边距,靠左或靠右时和边界的距离. + */ + private static final int PADDING_HORI = 6; + + /** + * 纵向边距,靠上或靠底时和边界的距离. + */ + private static final int PADDING_VERT = 6; + + + /** + * 图片中盒[左上角]的x坐标. + */ + private int boxPosX; + + /** + * 图片中盒[左上角]的y坐标. + */ + private int boxPosY; + + /** + * Instantiates a new image position. + * + * @param width the width + * @param height the height + * @param boxWidth the box width + * @param boxHeight the box height + * @param style the style + */ + public ImagePosition(int width, int height, int boxWidth, int boxHeight, int style) { + switch (style & 7) { + case LEFT: + boxPosX = PADDING_HORI; + break; + case RIGHT: + boxPosX = width - boxWidth - PADDING_HORI; + break; + case CENTER: + default: + boxPosX = (width - boxWidth) / 2; + } + switch (style >> 3 << 3) { + case TOP: + boxPosY = PADDING_VERT; + break; + case MIDDLE: + boxPosY = (height - boxHeight) / 2; + break; + case BOTTOM: + default: + boxPosY = height - boxHeight - PADDING_VERT; + } + } + + + /** + * Gets the x. + * + * @return the x + */ + public int getX() { + return getX(0); + } + + /** + * Gets the x. + * + * @param x 横向偏移 + * @return the x + */ + public int getX(int x) { + return this.boxPosX + x; + } + + /** + * Gets the y. + * + * @return the y + */ + public int getY() { + return getY(0); + } + + /** + * Gets the y. + * + * @param y 纵向偏移 + * @return the y + */ + public int getY(int y) { + return this.boxPosY + y; + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/support/Kv.java b/blade-core-tool/src/main/java/org/springblade/core/tool/support/Kv.java new file mode 100644 index 0000000..80faab8 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/support/Kv.java @@ -0,0 +1,239 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.tool.support; + +import org.springblade.core.tool.utils.Func; +import org.springframework.util.LinkedCaseInsensitiveMap; + +import java.sql.Time; +import java.sql.Timestamp; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +/** + * 链式map + * + * @author Chill + */ +public class Kv extends LinkedCaseInsensitiveMap { + + private Kv() { + super(); + } + + /** + * 创建Kv + * + * @return Kv + */ + public static Kv init() { + return new Kv(); + } + + /** + * 创建Kv + * + * @return Kv + */ + public static Kv create() { + return new Kv(); + } + + public static HashMap newMap() { + return new HashMap<>(16); + } + + /** + * 设置列 + * + * @param attr 属性 + * @param value 值 + * @return 本身 + */ + public Kv set(String attr, Object value) { + this.put(attr, value); + return this; + } + + /** + * 设置全部 + * + * @param map 属性 + * @return 本身 + */ + public Kv setAll(Map map) { + if (map != null) { + this.putAll(map); + } + return this; + } + + /** + * 设置列,当键或值为null时忽略 + * + * @param attr 属性 + * @param value 值 + * @return 本身 + */ + public Kv setIgnoreNull(String attr, Object value) { + if (attr != null && value != null) { + set(attr, value); + } + return this; + } + + public Object getObj(String key) { + return super.get(key); + } + + /** + * 获得特定类型值 + * + * @param 值类型 + * @param attr 字段名 + * @param defaultValue 默认值 + * @return 字段值 + */ + @SuppressWarnings("unchecked") + public T get(String attr, T defaultValue) { + final Object result = get(attr); + return (T) (result != null ? result : defaultValue); + } + + /** + * 获得特定类型值 + * + * @param attr 字段名 + * @return 字段值 + */ + public String getStr(String attr) { + return Func.toStr(get(attr), null); + } + + /** + * 获得特定类型值 + * + * @param attr 字段名 + * @return 字段值 + */ + public Integer getInt(String attr) { + return Func.toInt(get(attr), -1); + } + + /** + * 获得特定类型值 + * + * @param attr 字段名 + * @return 字段值 + */ + public Long getLong(String attr) { + return Func.toLong(get(attr), -1L); + } + + /** + * 获得特定类型值 + * + * @param attr 字段名 + * @return 字段值 + */ + public Float getFloat(String attr) { + return Func.toFloat(get(attr), null); + } + + public Double getDouble(String attr) { + return Func.toDouble(get(attr), null); + } + + + /** + * 获得特定类型值 + * + * @param attr 字段名 + * @return 字段值 + */ + public Boolean getBool(String attr) { + return Func.toBoolean(get(attr), null); + } + + /** + * 获得特定类型值 + * + * @param attr 字段名 + * @return 字段值 + */ + public byte[] getBytes(String attr) { + return get(attr, null); + } + + /** + * 获得特定类型值 + * + * @param attr 字段名 + * @return 字段值 + */ + public Date getDate(String attr) { + return get(attr, null); + } + + /** + * 获得特定类型值 + * + * @param attr 字段名 + * @return 字段值 + */ + public Time getTime(String attr) { + return get(attr, null); + } + + /** + * 获得特定类型值 + * + * @param attr 字段名 + * @return 字段值 + */ + public Timestamp getTimestamp(String attr) { + return get(attr, null); + } + + /** + * 获得特定类型值 + * + * @param attr 字段名 + * @return 字段值 + */ + public Number getNumber(String attr) { + return get(attr, null); + } + + @Override + public Kv clone() { + Kv clone = new Kv(); + clone.putAll(this); + return clone; + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/support/StrSpliter.java b/blade-core-tool/src/main/java/org/springblade/core/tool/support/StrSpliter.java new file mode 100644 index 0000000..377e9b5 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/support/StrSpliter.java @@ -0,0 +1,501 @@ +package org.springblade.core.tool.support; + +import org.springblade.core.tool.utils.Func; +import org.springblade.core.tool.utils.StringPool; +import org.springblade.core.tool.utils.StringUtil; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * 字符串切分器 + * + * @author Looly + */ +public class StrSpliter { + + //---------------------------------------------------------------------------------------------- Split by char + + /** + * 切分字符串路径,仅支持Unix分界符:/ + * + * @param str 被切分的字符串 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static List splitPath(String str) { + return splitPath(str, 0); + } + + /** + * 切分字符串路径,仅支持Unix分界符:/ + * + * @param str 被切分的字符串 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static String[] splitPathToArray(String str) { + return toArray(splitPath(str)); + } + + /** + * 切分字符串路径,仅支持Unix分界符:/ + * + * @param str 被切分的字符串 + * @param limit 限制分片数 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static List splitPath(String str, int limit) { + return split(str, StringPool.SLASH, limit, true, true); + } + + /** + * 切分字符串路径,仅支持Unix分界符:/ + * + * @param str 被切分的字符串 + * @param limit 限制分片数 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static String[] splitPathToArray(String str, int limit) { + return toArray(splitPath(str, limit)); + } + + /** + * 切分字符串 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.2.1 + */ + public static List splitTrim(String str, char separator, boolean ignoreEmpty) { + return split(str, separator, 0, true, ignoreEmpty); + } + + /** + * 切分字符串 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @param isTrim 是否去除切分字符串后每个元素两边的空格 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static List split(String str, char separator, boolean isTrim, boolean ignoreEmpty) { + return split(str, separator, 0, isTrim, ignoreEmpty); + } + + /** + * 切分字符串,大小写敏感,去除每个元素两边空白符 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @param limit 限制分片数,-1不限制 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static List splitTrim(String str, char separator, int limit, boolean ignoreEmpty) { + return split(str, separator, limit, true, ignoreEmpty, false); + } + + /** + * 切分字符串,大小写敏感 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @param limit 限制分片数,-1不限制 + * @param isTrim 是否去除切分字符串后每个元素两边的空格 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static List split(String str, char separator, int limit, boolean isTrim, boolean ignoreEmpty) { + return split(str, separator, limit, isTrim, ignoreEmpty, false); + } + + /** + * 切分字符串,忽略大小写 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @param limit 限制分片数,-1不限制 + * @param isTrim 是否去除切分字符串后每个元素两边的空格 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.2.1 + */ + public static List splitIgnoreCase(String str, char separator, int limit, boolean isTrim, boolean ignoreEmpty) { + return split(str, separator, limit, isTrim, ignoreEmpty, true); + } + + /** + * 切分字符串 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @param limit 限制分片数,-1不限制 + * @param isTrim 是否去除切分字符串后每个元素两边的空格 + * @param ignoreEmpty 是否忽略空串 + * @param ignoreCase 是否忽略大小写 + * @return 切分后的集合 + * @since 3.2.1 + */ + public static List split(String str, char separator, int limit, boolean isTrim, boolean ignoreEmpty, boolean ignoreCase) { + if (StringUtil.isEmpty(str)) { + return new ArrayList(0); + } + if (limit == 1) { + return addToList(new ArrayList(1), str, isTrim, ignoreEmpty); + } + + final ArrayList list = new ArrayList<>(limit > 0 ? limit : 16); + int len = str.length(); + int start = 0; + for (int i = 0; i < len; i++) { + if (Func.equals(separator, str.charAt(i))) { + addToList(list, str.substring(start, i), isTrim, ignoreEmpty); + start = i + 1; + + //检查是否超出范围(最大允许limit-1个,剩下一个留给末尾字符串) + if (limit > 0 && list.size() > limit - 2) { + break; + } + } + } + return addToList(list, str.substring(start, len), isTrim, ignoreEmpty); + } + + /** + * 切分字符串为字符串数组 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @param limit 限制分片数 + * @param isTrim 是否去除切分字符串后每个元素两边的空格 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static String[] splitToArray(String str, char separator, int limit, boolean isTrim, boolean ignoreEmpty) { + return toArray(split(str, separator, limit, isTrim, ignoreEmpty)); + } + + //---------------------------------------------------------------------------------------------- Split by String + + /** + * 切分字符串,不忽略大小写 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符串 + * @param isTrim 是否去除切分字符串后每个元素两边的空格 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static List split(String str, String separator, boolean isTrim, boolean ignoreEmpty) { + return split(str, separator, -1, isTrim, ignoreEmpty, false); + } + + /** + * 切分字符串,去除每个元素两边空格,忽略大小写 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符串 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.2.1 + */ + public static List splitTrim(String str, String separator, boolean ignoreEmpty) { + return split(str, separator, true, ignoreEmpty); + } + + /** + * 切分字符串,不忽略大小写 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符串 + * @param limit 限制分片数 + * @param isTrim 是否去除切分字符串后每个元素两边的空格 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static List split(String str, String separator, int limit, boolean isTrim, boolean ignoreEmpty) { + return split(str, separator, limit, isTrim, ignoreEmpty, false); + } + + /** + * 切分字符串,去除每个元素两边空格,忽略大小写 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符串 + * @param limit 限制分片数 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.2.1 + */ + public static List splitTrim(String str, String separator, int limit, boolean ignoreEmpty) { + return split(str, separator, limit, true, ignoreEmpty); + } + + /** + * 切分字符串,忽略大小写 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符串 + * @param limit 限制分片数 + * @param isTrim 是否去除切分字符串后每个元素两边的空格 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.2.1 + */ + public static List splitIgnoreCase(String str, String separator, int limit, boolean isTrim, boolean ignoreEmpty) { + return split(str, separator, limit, isTrim, ignoreEmpty, true); + } + + /** + * 切分字符串,去除每个元素两边空格,忽略大小写 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符串 + * @param limit 限制分片数 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.2.1 + */ + public static List splitTrimIgnoreCase(String str, String separator, int limit, boolean ignoreEmpty) { + return split(str, separator, limit, true, ignoreEmpty, true); + } + + /** + * 切分字符串 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符串 + * @param limit 限制分片数 + * @param isTrim 是否去除切分字符串后每个元素两边的空格 + * @param ignoreEmpty 是否忽略空串 + * @param ignoreCase 是否忽略大小写 + * @return 切分后的集合 + * @since 3.2.1 + */ + public static List split(String str, String separator, int limit, boolean isTrim, boolean ignoreEmpty, boolean ignoreCase) { + if (StringUtil.isEmpty(str)) { + return new ArrayList(0); + } + if (limit == 1) { + return addToList(new ArrayList(1), str, isTrim, ignoreEmpty); + } + + if (StringUtil.isEmpty(separator)) { + return split(str, limit); + } else if (separator.length() == 1) { + return split(str, separator.charAt(0), limit, isTrim, ignoreEmpty, ignoreCase); + } + + final ArrayList list = new ArrayList<>(); + int len = str.length(); + int separatorLen = separator.length(); + int start = 0; + int i = 0; + while (i < len) { + i = StringUtil.indexOf(str, separator, start, ignoreCase); + if (i > -1) { + addToList(list, str.substring(start, i), isTrim, ignoreEmpty); + start = i + separatorLen; + + //检查是否超出范围(最大允许limit-1个,剩下一个留给末尾字符串) + if (limit > 0 && list.size() > limit - 2) { + break; + } + } else { + break; + } + } + return addToList(list, str.substring(start, len), isTrim, ignoreEmpty); + } + + /** + * 切分字符串为字符串数组 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @param limit 限制分片数 + * @param isTrim 是否去除切分字符串后每个元素两边的空格 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static String[] splitToArray(String str, String separator, int limit, boolean isTrim, boolean ignoreEmpty) { + return toArray(split(str, separator, limit, isTrim, ignoreEmpty)); + } + + //---------------------------------------------------------------------------------------------- Split by Whitespace + + /** + * 使用空白符切分字符串
+ * 切分后的字符串两边不包含空白符,空串或空白符串并不做为元素之一 + * + * @param str 被切分的字符串 + * @param limit 限制分片数 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static List split(String str, int limit) { + if (StringUtil.isEmpty(str)) { + return new ArrayList(0); + } + if (limit == 1) { + return addToList(new ArrayList(1), str, true, true); + } + + final ArrayList list = new ArrayList<>(); + int len = str.length(); + int start = 0; + for (int i = 0; i < len; i++) { + if (Func.isEmpty(str.charAt(i))) { + addToList(list, str.substring(start, i), true, true); + start = i + 1; + if (limit > 0 && list.size() > limit - 2) { + break; + } + } + } + return addToList(list, str.substring(start, len), true, true); + } + + /** + * 切分字符串为字符串数组 + * + * @param str 被切分的字符串 + * @param limit 限制分片数 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static String[] splitToArray(String str, int limit) { + return toArray(split(str, limit)); + } + + //---------------------------------------------------------------------------------------------- Split by regex + + /** + * 通过正则切分字符串 + * + * @param str 字符串 + * @param separatorPattern 分隔符正则{@link Pattern} + * @param limit 限制分片数 + * @param isTrim 是否去除切分字符串后每个元素两边的空格 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static List split(String str, Pattern separatorPattern, int limit, boolean isTrim, boolean ignoreEmpty) { + if (StringUtil.isEmpty(str)) { + return new ArrayList(0); + } + if (limit == 1) { + return addToList(new ArrayList(1), str, isTrim, ignoreEmpty); + } + + if (null == separatorPattern) { + return split(str, limit); + } + + final Matcher matcher = separatorPattern.matcher(str); + final ArrayList list = new ArrayList<>(); + int len = str.length(); + int start = 0; + while (matcher.find()) { + addToList(list, str.substring(start, matcher.start()), isTrim, ignoreEmpty); + start = matcher.end(); + + if (limit > 0 && list.size() > limit - 2) { + break; + } + } + return addToList(list, str.substring(start, len), isTrim, ignoreEmpty); + } + + /** + * 通过正则切分字符串为字符串数组 + * + * @param str 被切分的字符串 + * @param separatorPattern 分隔符正则{@link Pattern} + * @param limit 限制分片数 + * @param isTrim 是否去除切分字符串后每个元素两边的空格 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static String[] splitToArray(String str, Pattern separatorPattern, int limit, boolean isTrim, boolean ignoreEmpty) { + return toArray(split(str, separatorPattern, limit, isTrim, ignoreEmpty)); + } + + //---------------------------------------------------------------------------------------------- Split by length + + /** + * 根据给定长度,将给定字符串截取为多个部分 + * + * @param str 字符串 + * @param len 每一个小节的长度 + * @return 截取后的字符串数组 + */ + public static String[] splitByLength(String str, int len) { + int partCount = str.length() / len; + int lastPartCount = str.length() % len; + int fixPart = 0; + if (lastPartCount != 0) { + fixPart = 1; + } + + final String[] strs = new String[partCount + fixPart]; + for (int i = 0; i < partCount + fixPart; i++) { + if (i == partCount + fixPart - 1 && lastPartCount != 0) { + strs[i] = str.substring(i * len, i * len + lastPartCount); + } else { + strs[i] = str.substring(i * len, i * len + len); + } + } + return strs; + } + + //---------------------------------------------------------------------------------------------------------- Private method start + + /** + * 将字符串加入List中 + * + * @param list 列表 + * @param part 被加入的部分 + * @param isTrim 是否去除两端空白符 + * @param ignoreEmpty 是否略过空字符串(空字符串不做为一个元素) + * @return 列表 + */ + private static List addToList(List list, String part, boolean isTrim, boolean ignoreEmpty) { + part = part.toString(); + if (isTrim) { + part = part.trim(); + } + if (false == ignoreEmpty || false == part.isEmpty()) { + list.add(part); + } + return list; + } + + /** + * List转Array + * + * @param list List + * @return Array + */ + private static String[] toArray(List list) { + return list.toArray(new String[list.size()]); + } + //---------------------------------------------------------------------------------------------------------- Private method end +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/support/Try.java b/blade-core-tool/src/main/java/org/springblade/core/tool/support/Try.java new file mode 100644 index 0000000..c453b50 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/support/Try.java @@ -0,0 +1,88 @@ +package org.springblade.core.tool.support; + +import org.springblade.core.tool.utils.Exceptions; +import org.springframework.lang.Nullable; + +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * Lambda 受检异常处理 + * https://segmentfault.com/a/1190000007832130 + * + * @author Chill + */ +public class Try { + + public static Function of(UncheckedFunction mapper) { + Objects.requireNonNull(mapper); + return t -> { + try { + return mapper.apply(t); + } catch (Exception e) { + throw Exceptions.unchecked(e); + } + }; + } + + public static Consumer of(UncheckedConsumer mapper) { + Objects.requireNonNull(mapper); + return t -> { + try { + mapper.accept(t); + } catch (Exception e) { + throw Exceptions.unchecked(e); + } + }; + } + + public static Supplier of(UncheckedSupplier mapper) { + Objects.requireNonNull(mapper); + return () -> { + try { + return mapper.get(); + } catch (Exception e) { + throw Exceptions.unchecked(e); + } + }; + } + + @FunctionalInterface + public interface UncheckedFunction { + /** + * apply + * + * @param t + * @return + * @throws Exception + */ + @Nullable + R apply(@Nullable T t) throws Exception; + } + + @FunctionalInterface + public interface UncheckedConsumer { + /** + * accept + * + * @param t + * @throws Exception + */ + @Nullable + void accept(@Nullable T t) throws Exception; + } + + @FunctionalInterface + public interface UncheckedSupplier { + /** + * get + * + * @return + * @throws Exception + */ + @Nullable + T get() throws Exception; + } +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/tuple/KeyPair.java b/blade-core-tool/src/main/java/org/springblade/core/tool/tuple/KeyPair.java new file mode 100644 index 0000000..6fbc9e3 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/tuple/KeyPair.java @@ -0,0 +1,71 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.tool.tuple; + +import lombok.RequiredArgsConstructor; +import org.springblade.core.tool.utils.RsaUtil; + +import java.security.PrivateKey; +import java.security.PublicKey; + +/** + * rsa 的 key pair 封装 + * + * @author L.cm + */ +@RequiredArgsConstructor +public class KeyPair { + private final java.security.KeyPair keyPair; + + public PublicKey getPublic() { + return keyPair.getPublic(); + } + + public PrivateKey getPrivate() { + return keyPair.getPrivate(); + } + + public byte[] getPublicBytes() { + return this.getPublic().getEncoded(); + } + + public byte[] getPrivateBytes() { + return this.getPrivate().getEncoded(); + } + + public String getPublicBase64() { + return RsaUtil.getKeyString(this.getPublic()); + } + + public String getPrivateBase64() { + return RsaUtil.getKeyString(this.getPrivate()); + } + + @Override + public String toString() { + return "PublicKey=" + this.getPublicBase64() + '\n' + "PrivateKey=" + this.getPrivateBase64(); + } +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/tuple/Pair.java b/blade-core-tool/src/main/java/org/springblade/core/tool/tuple/Pair.java new file mode 100644 index 0000000..b0d3fde --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/tuple/Pair.java @@ -0,0 +1,93 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.tool.tuple; + +import lombok.*; + +/** + * tuple Pair + * + * @author L.cm + **/ +@Getter +@ToString +@EqualsAndHashCode +public class Pair { + private static final Pair EMPTY = new Pair<>(null, null); + + private final L left; + private final R right; + + /** + * Returns an empty pair. + */ + @SuppressWarnings("unchecked") + public static Pair empty() { + return (Pair) EMPTY; + } + + /** + * Constructs a pair with its left value being {@code left}, or returns an empty pair if + * {@code left} is null. + * + * @return the constructed pair or an empty pair if {@code left} is null. + */ + public static Pair createLeft(L left) { + if (left == null) { + return empty(); + } else { + return new Pair<>(left, null); + } + } + + /** + * Constructs a pair with its right value being {@code right}, or returns an empty pair if + * {@code right} is null. + * + * @return the constructed pair or an empty pair if {@code right} is null. + */ + public static Pair createRight(R right) { + if (right == null) { + return empty(); + } else { + return new Pair<>(null, right); + } + } + + public static Pair create(L left, R right) { + if (right == null && left == null) { + return empty(); + } else { + return new Pair<>(left, right); + } + } + + private Pair(L left, R right) { + this.left = left; + this.right = right; + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/AesUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/AesUtil.java new file mode 100644 index 0000000..b0c54f2 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/AesUtil.java @@ -0,0 +1,259 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.tool.utils; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +import javax.crypto.Cipher; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Objects; +import java.util.function.Function; + +/** + * 完全兼容微信所使用的AES加密工具类 + * aes的key必须是256byte长(比如32个字符),可以使用AesKit.genAesKey()来生成一组key + * + * @author L.cm + */ +public class AesUtil { + public static final Charset DEFAULT_CHARSET = Charsets.UTF_8; + + public static String genAesKey() { + return StringUtil.random(32); + } + + /** + * 转换成mysql aes + * + * @param key key + * @return SecretKeySpec + */ + public static SecretKeySpec genMySqlAesKey(final byte[] key) { + final byte[] finalKey = new byte[16]; + int i = 0; + for (byte b : key) { + finalKey[i++ % 16] ^= b; + } + return new SecretKeySpec(finalKey, "AES"); + } + + /** + * 转换成mysql aes + * + * @param key key + * @return SecretKeySpec + */ + public static SecretKeySpec genMySqlAesKey(final String key) { + return genMySqlAesKey(key.getBytes(DEFAULT_CHARSET)); + } + + public static String encryptToHex(String content, String aesTextKey) { + return HexUtil.encodeToString(encrypt(content, aesTextKey)); + } + + public static String encryptToHex(byte[] content, String aesTextKey) { + return HexUtil.encodeToString(encrypt(content, aesTextKey)); + } + + public static String encryptToBase64(String content, String aesTextKey) { + return Base64Util.encodeToString(encrypt(content, aesTextKey)); + } + + public static String encryptToBase64(byte[] content, String aesTextKey) { + return Base64Util.encodeToString(encrypt(content, aesTextKey)); + } + + public static byte[] encrypt(String content, String aesTextKey) { + return encrypt(content.getBytes(DEFAULT_CHARSET), aesTextKey); + } + + public static byte[] encrypt(String content, Charset charset, String aesTextKey) { + return encrypt(content.getBytes(charset), aesTextKey); + } + + public static byte[] encrypt(byte[] content, String aesTextKey) { + return encrypt(content, Objects.requireNonNull(aesTextKey).getBytes(DEFAULT_CHARSET)); + } + + @Nullable + public static String decryptFormHexToString(@Nullable String content, String aesTextKey) { + byte[] hexBytes = decryptFormHex(content, aesTextKey); + if (hexBytes == null) { + return null; + } + return new String(hexBytes, DEFAULT_CHARSET); + } + + @Nullable + public static byte[] decryptFormHex(@Nullable String content, String aesTextKey) { + if (StringUtil.isBlank(content)) { + return null; + } + return decryptFormHex(content.getBytes(DEFAULT_CHARSET), aesTextKey); + } + + public static byte[] decryptFormHex(byte[] content, String aesTextKey) { + return decrypt(HexUtil.decode(content), aesTextKey); + } + + @Nullable + public static String decryptFormBase64ToString(@Nullable String content, String aesTextKey) { + byte[] hexBytes = decryptFormBase64(content, aesTextKey); + if (hexBytes == null) { + return null; + } + return new String(hexBytes, DEFAULT_CHARSET); + } + + @Nullable + public static byte[] decryptFormBase64(@Nullable String content, String aesTextKey) { + if (StringUtil.isBlank(content)) { + return null; + } + return decryptFormBase64(content.getBytes(DEFAULT_CHARSET), aesTextKey); + } + + public static byte[] decryptFormBase64(byte[] content, String aesTextKey) { + return decrypt(Base64Util.decode(content), aesTextKey); + } + + public static String decryptToString(byte[] content, String aesTextKey) { + return new String(decrypt(content, aesTextKey), DEFAULT_CHARSET); + } + + public static byte[] decrypt(byte[] content, String aesTextKey) { + return decrypt(content, Objects.requireNonNull(aesTextKey).getBytes(DEFAULT_CHARSET)); + } + + public static byte[] encrypt(byte[] content, byte[] aesKey) { + return aes(Pkcs7Encoder.encode(content), aesKey, Cipher.ENCRYPT_MODE); + } + + public static byte[] decrypt(byte[] encrypted, byte[] aesKey) { + return Pkcs7Encoder.decode(aes(encrypted, aesKey, Cipher.DECRYPT_MODE)); + } + + private static byte[] aes(byte[] encrypted, byte[] aesKey, int mode) { + Assert.isTrue(aesKey.length == 32, "IllegalAesKey, aesKey's length must be 32"); + try { + Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); + SecretKeySpec keySpec = new SecretKeySpec(aesKey, "AES"); + IvParameterSpec iv = new IvParameterSpec(Arrays.copyOfRange(aesKey, 0, 16)); + cipher.init(mode, keySpec, iv); + return cipher.doFinal(encrypted); + } catch (Exception e) { + throw Exceptions.unchecked(e); + } + } + + + /** + * 兼容 mysql 的 aes 加密 + * + * @param input input + * @param aesKey aesKey + * @return byte array + */ + public static byte[] encryptMysql(String input, String aesKey) { + return encryptMysql(input, aesKey, Function.identity()); + } + + /** + * 兼容 mysql 的 aes 加密 + * + * @param input input + * @param aesKey aesKey + * @param 泛型标记 + * @return T 泛型对象 + */ + public static T encryptMysql(String input, String aesKey, Function mapper) { + try { + Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); + cipher.init(Cipher.ENCRYPT_MODE, genMySqlAesKey(aesKey)); + byte[] bytes = cipher.doFinal(input.getBytes(StandardCharsets.UTF_8)); + return mapper.apply(bytes); + } catch (Exception e) { + throw Exceptions.unchecked(e); + } + } + + /** + * 兼容 mysql 的 aes 加密 + * + * @param input input + * @param aesKey aesKey + * @return byte 数组 + */ + public static byte[] decryptMysql(String input, String aesKey) { + return decryptMysql(input, txt -> txt.getBytes(DEFAULT_CHARSET), aesKey); + } + + /** + * 兼容 mysql 的 aes 加密 + * + * @param input input + * @param aesKey aesKey + * @return byte 数组 + */ + public static byte[] decryptMysql(String input, Function inputMapper, String aesKey) { + try { + Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); + cipher.init(Cipher.DECRYPT_MODE, genMySqlAesKey(aesKey)); + return cipher.doFinal(inputMapper.apply(input)); + } catch (Exception e) { + throw Exceptions.unchecked(e); + } + } + + /** + * 兼容 mysql 的 aes 加密 + * + * @param input input + * @param inputMapper Function + * @param aesKey aesKey + * @return 字符串 + */ + public static String decryptMysqlToString(String input, Function inputMapper, String aesKey) { + return new String(decryptMysql(input, inputMapper, aesKey), DEFAULT_CHARSET); + } + + /** + * 兼容 mysql 的 aes 加密 + * + * @param input input + * @param aesKey aesKey + * @return 字符串 + */ + public static String decryptMysqlToString(String input, String aesKey) { + return decryptMysqlToString(input, txt -> txt.getBytes(DEFAULT_CHARSET), aesKey); + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/AntPathFilter.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/AntPathFilter.java new file mode 100644 index 0000000..0a00878 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/AntPathFilter.java @@ -0,0 +1,60 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.tool.utils; + +import lombok.AllArgsConstructor; +import org.springframework.util.AntPathMatcher; +import org.springframework.util.PathMatcher; + +import java.io.File; +import java.io.FileFilter; +import java.io.Serializable; + +/** + * Spring AntPath 规则文件过滤 + * + * @author L.cm + */ +@AllArgsConstructor +public class AntPathFilter implements FileFilter, Serializable { + private static final long serialVersionUID = 812598009067554612L; + private static final PathMatcher PATH_MATCHER = new AntPathMatcher(); + + private final String pattern; + + /** + * 过滤规则 + * + * @param pathname 路径 + * @return boolean + */ + @Override + public boolean accept(File pathname) { + String filePath = pathname.getAbsolutePath(); + return PATH_MATCHER.match(pattern, filePath); + } +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/Base64Util.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/Base64Util.java new file mode 100644 index 0000000..0fa8ae9 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/Base64Util.java @@ -0,0 +1,235 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.tool.utils; + +import java.nio.charset.Charset; +import java.util.Base64; + +/** + * Base64工具 + * + * @author L.cm + */ +public class Base64Util { + public static final Base64.Encoder ENCODER = Base64.getEncoder(); + public static final Base64.Encoder URL_ENCODER = Base64.getUrlEncoder(); + public static final Base64.Decoder DECODER = Base64.getDecoder(); + public static final Base64.Decoder URL_DECODER = Base64.getUrlDecoder(); + + /** + * 编码 + * + * @param value 字符串 + * @return {String} + */ + public static String encode(String value) { + return encode(value, Charsets.UTF_8); + } + + /** + * 编码 + * + * @param value 字符串 + * @param charset 字符集 + * @return {String} + */ + public static String encode(String value, Charset charset) { + byte[] val = value.getBytes(charset); + return new String(encode(val), charset); + } + + /** + * 编码URL安全 + * + * @param value 字符串 + * @return {String} + */ + public static String encodeUrlSafe(String value) { + return encodeUrlSafe(value, Charsets.UTF_8); + } + + /** + * 编码URL安全 + * + * @param value 字符串 + * @param charset 字符集 + * @return {String} + */ + public static String encodeUrlSafe(String value, Charset charset) { + byte[] val = value.getBytes(charset); + return new String(encodeUrlSafe(val), charset); + } + + /** + * 解码 + * + * @param value 字符串 + * @return {String} + */ + public static String decode(String value) { + return decode(value, Charsets.UTF_8); + } + + /** + * 解码 + * + * @param value 字符串 + * @param charset 字符集 + * @return {String} + */ + public static String decode(String value, Charset charset) { + byte[] val = value.getBytes(charset); + byte[] decodedValue = decode(val); + return new String(decodedValue, charset); + } + + /** + * 解码URL安全 + * + * @param value 字符串 + * @return {String} + */ + public static String decodeUrlSafe(String value) { + return decodeUrlSafe(value, Charsets.UTF_8); + } + + /** + * 解码URL安全 + * + * @param value 字符串 + * @param charset 字符集 + * @return {String} + */ + public static String decodeUrlSafe(String value, Charset charset) { + byte[] val = value.getBytes(charset); + byte[] decodedValue = decodeUrlSafe(val); + return new String(decodedValue, charset); + } + + /** + * Base64-encode the given byte array. + * + * @param src the original byte array + * @return the encoded byte array + */ + public static byte[] encode(byte[] src) { + if (src.length == 0) { + return src; + } + return ENCODER.encode(src); + } + + /** + * Base64-decode the given byte array. + * + * @param src the encoded byte array + * @return the original byte array + */ + public static byte[] decode(byte[] src) { + if (src.length == 0) { + return src; + } + return DECODER.decode(src); + } + + /** + * Base64-encode the given byte array using the RFC 4648 + * "URL and Filename Safe Alphabet". + * + * @param src the original byte array + * @return the encoded byte array + */ + public static byte[] encodeUrlSafe(byte[] src) { + if (src.length == 0) { + return src; + } + return URL_ENCODER.encode(src); + } + + /** + * Base64-decode the given byte array using the RFC 4648 + * "URL and Filename Safe Alphabet". + * + * @param src the encoded byte array + * @return the original byte array + * @since 4.2.4 + */ + public static byte[] decodeUrlSafe(byte[] src) { + if (src.length == 0) { + return src; + } + return URL_DECODER.decode(src); + } + + /** + * Base64-encode the given byte array to a String. + * + * @param src the original byte array + * @return the encoded byte array as a UTF-8 String + */ + public static String encodeToString(byte[] src) { + if (src.length == 0) { + return ""; + } + return new String(encode(src), Charsets.UTF_8); + } + + /** + * Base64-decode the given byte array from an UTF-8 String. + * + * @param src the encoded UTF-8 String + * @return the original byte array + */ + public static byte[] decodeFromString(String src) { + if (src.isEmpty()) { + return new byte[0]; + } + return decode(src.getBytes(Charsets.UTF_8)); + } + + /** + * Base64-encode the given byte array to a String using the RFC 4648 + * "URL and Filename Safe Alphabet". + * + * @param src the original byte array + * @return the encoded byte array as a UTF-8 String + */ + public static String encodeToUrlSafeString(byte[] src) { + return new String(encodeUrlSafe(src), Charsets.UTF_8); + } + + /** + * Base64-decode the given byte array from an UTF-8 String using the RFC 4648 + * "URL and Filename Safe Alphabet". + * + * @param src the encoded UTF-8 String + * @return the original byte array + */ + public static byte[] decodeFromUrlSafeString(String src) { + return decodeUrlSafe(src.getBytes(Charsets.UTF_8)); + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/BeanUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/BeanUtil.java new file mode 100644 index 0000000..8a0bf43 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/BeanUtil.java @@ -0,0 +1,433 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.tool.utils; + + +import org.springblade.core.tool.beans.BeanProperty; +import org.springblade.core.tool.beans.BladeBeanCopier; +import org.springblade.core.tool.beans.BladeBeanMap; +import org.springblade.core.tool.convert.BladeConverter; +import org.springframework.beans.BeanWrapper; +import org.springframework.beans.BeansException; +import org.springframework.beans.PropertyAccessorFactory; +import org.springframework.cglib.beans.BeanGenerator; +import org.springframework.lang.Nullable; + +import java.util.*; + +/** + * 实体工具类 + * + * @author L.cm + */ +public class BeanUtil extends org.springframework.beans.BeanUtils { + + /** + * 实例化对象 + * + * @param clazz 类 + * @param 泛型标记 + * @return 对象 + */ + @SuppressWarnings("unchecked") + public static T newInstance(Class clazz) { + return (T) instantiateClass(clazz); + } + + /** + * 实例化对象 + * + * @param clazzStr 类名 + * @param 泛型标记 + * @return 对象 + */ + public static T newInstance(String clazzStr) { + try { + Class clazz = ClassUtil.forName(clazzStr, null); + return newInstance(clazz); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + + /** + * 获取Bean的属性, 支持 propertyName 多级 :test.user.name + * + * @param bean bean + * @param propertyName 属性名 + * @return 属性值 + */ + @Nullable + public static Object getProperty(@Nullable Object bean, String propertyName) { + if (bean == null) { + return null; + } + BeanWrapper beanWrapper = PropertyAccessorFactory.forBeanPropertyAccess(bean); + return beanWrapper.getPropertyValue(propertyName); + } + + /** + * 设置Bean属性, 支持 propertyName 多级 :test.user.name + * + * @param bean bean + * @param propertyName 属性名 + * @param value 属性值 + */ + public static void setProperty(Object bean, String propertyName, Object value) { + Objects.requireNonNull(bean, "bean Could not null"); + BeanWrapper beanWrapper = PropertyAccessorFactory.forBeanPropertyAccess(bean); + beanWrapper.setPropertyValue(propertyName, value); + } + + /** + * 深复制 + * + *

+ * 支持 map bean + *

+ * + * @param source 源对象 + * @param 泛型标记 + * @return T + */ + @SuppressWarnings("unchecked") + @Nullable + public static T clone(@Nullable T source) { + if (source == null) { + return null; + } + return (T) BeanUtil.copy(source, source.getClass()); + } + + /** + * copy 对象属性,默认不使用Convert + * + *

+ * 支持 map bean copy + *

+ * + * @param source 源对象 + * @param clazz 类名 + * @param 泛型标记 + * @return T + */ + @Nullable + public static T copy(@Nullable Object source, Class clazz) { + if (source == null) { + return null; + } + return BeanUtil.copy(source, source.getClass(), clazz); + } + + /** + * copy 对象属性,默认不使用Convert + * + *

+ * 支持 map bean copy + *

+ * + * @param source 源对象 + * @param sourceClazz 源类型 + * @param targetClazz 转换成的类型 + * @param 泛型标记 + * @return T + */ + @Nullable + public static T copy(@Nullable Object source, Class sourceClazz, Class targetClazz) { + if (source == null) { + return null; + } + BladeBeanCopier copier = BladeBeanCopier.create(sourceClazz, targetClazz, false); + T to = newInstance(targetClazz); + copier.copy(source, to, null); + return to; + } + + /** + * copy 列表对象,默认不使用Convert + * + *

+ * 支持 map bean copy + *

+ * + * @param sourceList 源列表 + * @param targetClazz 转换成的类型 + * @param 泛型标记 + * @return T + */ + public static List copy(@Nullable Collection sourceList, Class targetClazz) { + if (sourceList == null || sourceList.isEmpty()) { + return Collections.emptyList(); + } + List outList = new ArrayList<>(sourceList.size()); + Class sourceClazz = null; + for (Object source : sourceList) { + if (source == null) { + continue; + } + if (sourceClazz == null) { + sourceClazz = source.getClass(); + } + T bean = BeanUtil.copy(source, sourceClazz, targetClazz); + outList.add(bean); + } + return outList; + } + + /** + * 拷贝对象 + * + *

+ * 支持 map bean copy + *

+ * + * @param source 源对象 + * @param targetBean 需要赋值的对象 + */ + public static void copy(@Nullable Object source, @Nullable Object targetBean) { + if (source == null || targetBean == null) { + return; + } + BladeBeanCopier copier = BladeBeanCopier + .create(source.getClass(), targetBean.getClass(), false); + + copier.copy(source, targetBean, null); + } + + /** + * 拷贝对象,source 属性做 null 判断,Map 不支持,map 会做 instanceof 判断,不会 + * + *

+ * 支持 bean copy + *

+ * + * @param source 源对象 + * @param targetBean 需要赋值的对象 + */ + public static void copyNonNull(@Nullable Object source, @Nullable Object targetBean) { + if (source == null || targetBean == null) { + return; + } + BladeBeanCopier copier = BladeBeanCopier + .create(source.getClass(), targetBean.getClass(), false, true); + + copier.copy(source, targetBean, null); + } + + /** + * 拷贝对象并对不同类型属性进行转换 + * + *

+ * 支持 map bean copy + *

+ * + * @param source 源对象 + * @param targetClazz 转换成的类 + * @param 泛型标记 + * @return T + */ + @Nullable + public static T copyWithConvert(@Nullable Object source, Class targetClazz) { + if (source == null) { + return null; + } + return BeanUtil.copyWithConvert(source, source.getClass(), targetClazz); + } + + /** + * 拷贝对象并对不同类型属性进行转换 + * + *

+ * 支持 map bean copy + *

+ * + * @param source 源对象 + * @param sourceClazz 源类 + * @param targetClazz 转换成的类 + * @param 泛型标记 + * @return T + */ + @Nullable + public static T copyWithConvert(@Nullable Object source, Class sourceClazz, Class targetClazz) { + if (source == null) { + return null; + } + BladeBeanCopier copier = BladeBeanCopier.create(sourceClazz, targetClazz, true); + T to = newInstance(targetClazz); + copier.copy(source, to, new BladeConverter(sourceClazz, targetClazz)); + return to; + } + + /** + * 拷贝列表并对不同类型属性进行转换 + * + *

+ * 支持 map bean copy + *

+ * + * @param sourceList 源对象列表 + * @param targetClazz 转换成的类 + * @param 泛型标记 + * @return List + */ + public static List copyWithConvert(@Nullable Collection sourceList, Class targetClazz) { + if (sourceList == null || sourceList.isEmpty()) { + return Collections.emptyList(); + } + List outList = new ArrayList<>(sourceList.size()); + Class sourceClazz = null; + for (Object source : sourceList) { + if (source == null) { + continue; + } + if (sourceClazz == null) { + sourceClazz = source.getClass(); + } + T bean = BeanUtil.copyWithConvert(source, sourceClazz, targetClazz); + outList.add(bean); + } + return outList; + } + + /** + * Copy the property values of the given source bean into the target class. + *

Note: The source and target classes do not have to match or even be derived + * from each other, as long as the properties match. Any bean properties that the + * source bean exposes but the target bean does not will silently be ignored. + *

This is just a convenience method. For more complex transfer needs, + * + * @param source the source bean + * @param targetClazz the target bean class + * @param 泛型标记 + * @return T + * @throws BeansException if the copying failed + */ + @Nullable + public static T copyProperties(@Nullable Object source, Class targetClazz) throws BeansException { + if (source == null) { + return null; + } + T to = newInstance(targetClazz); + BeanUtil.copyProperties(source, to); + return to; + } + + /** + * Copy the property values of the given source bean into the target class. + *

Note: The source and target classes do not have to match or even be derived + * from each other, as long as the properties match. Any bean properties that the + * source bean exposes but the target bean does not will silently be ignored. + *

This is just a convenience method. For more complex transfer needs, + * + * @param sourceList the source list bean + * @param targetClazz the target bean class + * @param 泛型标记 + * @return List + * @throws BeansException if the copying failed + */ + public static List copyProperties(@Nullable Collection sourceList, Class targetClazz) throws BeansException { + if (sourceList == null || sourceList.isEmpty()) { + return Collections.emptyList(); + } + List outList = new ArrayList<>(sourceList.size()); + for (Object source : sourceList) { + if (source == null) { + continue; + } + T bean = BeanUtil.copyProperties(source, targetClazz); + outList.add(bean); + } + return outList; + } + + /** + * 将对象装成map形式 + * + * @param bean 源对象 + * @return {Map} + */ + @SuppressWarnings("unchecked") + public static Map toMap(@Nullable Object bean) { + if (bean == null) { + return new HashMap<>(0); + } + return BladeBeanMap.create(bean); + } + + /** + * 将map 转为 bean + * + * @param beanMap map + * @param valueType 对象类型 + * @param 泛型标记 + * @return {T} + */ + public static T toBean(Map beanMap, Class valueType) { + Objects.requireNonNull(beanMap, "beanMap Could not null"); + T to = newInstance(valueType); + if (beanMap.isEmpty()) { + return to; + } + BeanUtil.copy(beanMap, to); + return to; + } + + /** + * 给一个Bean添加字段 + * + * @param superBean 父级Bean + * @param props 新增属性 + * @return {Object} + */ + @Nullable + public static Object generator(@Nullable Object superBean, BeanProperty... props) { + if (superBean == null) { + return null; + } + Class superclass = superBean.getClass(); + Object genBean = generator(superclass, props); + BeanUtil.copy(superBean, genBean); + return genBean; + } + + /** + * 给一个class添加字段 + * + * @param superclass 父级 + * @param props 新增属性 + * @return {Object} + */ + public static Object generator(Class superclass, BeanProperty... props) { + BeanGenerator generator = new BeanGenerator(); + generator.setSuperclass(superclass); + generator.setUseCache(true); + for (BeanProperty prop : props) { + generator.addProperty(prop.getName(), prop.getType()); + } + return generator.create(); + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/CharPool.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/CharPool.java new file mode 100644 index 0000000..cef92aa --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/CharPool.java @@ -0,0 +1,83 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.tool.utils; + +/** + * char 常量池 + * + * @author L.cm + */ +public interface CharPool { + + // @formatter:off + char UPPER_A = 'A'; + char LOWER_A = 'a'; + char UPPER_Z = 'Z'; + char LOWER_Z = 'z'; + char DOT = '.'; + char AT = '@'; + char LEFT_BRACE = '{'; + char RIGHT_BRACE = '}'; + char LEFT_BRACKET = '('; + char RIGHT_BRACKET = ')'; + char DASH = '-'; + char PERCENT = '%'; + char PIPE = '|'; + char PLUS = '+'; + char QUESTION_MARK = '?'; + char EXCLAMATION_MARK = '!'; + char EQUALS = '='; + char AMPERSAND = '&'; + char ASTERISK = '*'; + char STAR = ASTERISK; + char BACK_SLASH = '\\'; + char COLON = ':'; + char COMMA = ','; + char DOLLAR = '$'; + char SLASH = '/'; + char HASH = '#'; + char HAT = '^'; + char LEFT_CHEV = '<'; + char NEWLINE = '\n'; + char N = 'n'; + char Y = 'y'; + char QUOTE = '\"'; + char RETURN = '\r'; + char TAB = '\t'; + char RIGHT_CHEV = '>'; + char SEMICOLON = ';'; + char SINGLE_QUOTE = '\''; + char BACKTICK = '`'; + char SPACE = ' '; + char TILDA = '~'; + char LEFT_SQ_BRACKET = '['; + char RIGHT_SQ_BRACKET = ']'; + char UNDERSCORE = '_'; + char ONE = '1'; + char ZERO = '0'; + // @formatter:on + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/Charsets.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/Charsets.java new file mode 100644 index 0000000..c30cd05 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/Charsets.java @@ -0,0 +1,69 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.tool.utils; + + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.charset.UnsupportedCharsetException; + +/** + * 字符集工具类 + * + * @author L.cm + */ +public class Charsets { + + /** + * 字符集ISO-8859-1 + */ + public static final Charset ISO_8859_1 = StandardCharsets.ISO_8859_1; + public static final String ISO_8859_1_NAME = ISO_8859_1.name(); + + /** + * 字符集GBK + */ + public static final Charset GBK = Charset.forName(StringPool.GBK); + public static final String GBK_NAME = GBK.name(); + + /** + * 字符集utf-8 + */ + public static final Charset UTF_8 = StandardCharsets.UTF_8; + public static final String UTF_8_NAME = UTF_8.name(); + + /** + * 转换为Charset对象 + * + * @param charsetName 字符集,为空则返回默认字符集 + * @return Charsets + * @throws UnsupportedCharsetException 编码不支持 + */ + public static Charset charset(String charsetName) throws UnsupportedCharsetException { + return StringUtil.isBlank(charsetName) ? Charset.defaultCharset() : Charset.forName(charsetName); + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/ClassUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/ClassUtil.java new file mode 100644 index 0000000..85cdd80 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/ClassUtil.java @@ -0,0 +1,139 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.tool.utils; + +import org.springframework.core.BridgeMethodResolver; +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.MethodParameter; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.core.annotation.SynthesizingMethodParameter; +import org.springframework.web.method.HandlerMethod; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; + +/** + * 类操作工具 + * + * @author L.cm + */ +public class ClassUtil extends org.springframework.util.ClassUtils { + + private static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new DefaultParameterNameDiscoverer(); + + /** + * 获取方法参数信息 + * + * @param constructor 构造器 + * @param parameterIndex 参数序号 + * @return {MethodParameter} + */ + public static MethodParameter getMethodParameter(Constructor constructor, int parameterIndex) { + MethodParameter methodParameter = new SynthesizingMethodParameter(constructor, parameterIndex); + methodParameter.initParameterNameDiscovery(PARAMETER_NAME_DISCOVERER); + return methodParameter; + } + + /** + * 获取方法参数信息 + * + * @param method 方法 + * @param parameterIndex 参数序号 + * @return {MethodParameter} + */ + public static MethodParameter getMethodParameter(Method method, int parameterIndex) { + MethodParameter methodParameter = new SynthesizingMethodParameter(method, parameterIndex); + methodParameter.initParameterNameDiscovery(PARAMETER_NAME_DISCOVERER); + return methodParameter; + } + + /** + * 获取Annotation + * + * @param method Method + * @param annotationType 注解类 + * @param 泛型标记 + * @return {Annotation} + */ + public static A getAnnotation(Method method, Class annotationType) { + Class targetClass = method.getDeclaringClass(); + // The method may be on an interface, but we need attributes from the target class. + // If the target class is null, the method will be unchanged. + Method specificMethod = ClassUtil.getMostSpecificMethod(method, targetClass); + // If we are dealing with method with generic parameters, find the original method. + specificMethod = BridgeMethodResolver.findBridgedMethod(specificMethod); + // 先找方法,再找方法上的类 + A annotation = AnnotatedElementUtils.findMergedAnnotation(specificMethod, annotationType); + ; + if (null != annotation) { + return annotation; + } + // 获取类上面的Annotation,可能包含组合注解,故采用spring的工具类 + return AnnotatedElementUtils.findMergedAnnotation(specificMethod.getDeclaringClass(), annotationType); + } + + /** + * 获取Annotation + * + * @param handlerMethod HandlerMethod + * @param annotationType 注解类 + * @param 泛型标记 + * @return {Annotation} + */ + public static A getAnnotation(HandlerMethod handlerMethod, Class annotationType) { + // 先找方法,再找方法上的类 + A annotation = handlerMethod.getMethodAnnotation(annotationType); + if (null != annotation) { + return annotation; + } + // 获取类上面的Annotation,可能包含组合注解,故采用spring的工具类 + Class beanType = handlerMethod.getBeanType(); + return AnnotatedElementUtils.findMergedAnnotation(beanType, annotationType); + } + + + /** + * 判断是否有注解 Annotation + * + * @param method Method + * @param annotationType 注解类 + * @param 泛型标记 + * @return {boolean} + */ + public static boolean isAnnotated(Method method, Class annotationType) { + // 先找方法,再找方法上的类 + boolean isMethodAnnotated = AnnotatedElementUtils.isAnnotated(method, annotationType); + if (isMethodAnnotated) { + return true; + } + // 获取类上面的Annotation,可能包含组合注解,故采用spring的工具类 + Class targetClass = method.getDeclaringClass(); + return AnnotatedElementUtils.isAnnotated(targetClass, annotationType); + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/CollectionUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/CollectionUtil.java new file mode 100644 index 0000000..4095f29 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/CollectionUtil.java @@ -0,0 +1,186 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.tool.utils; + +import org.springframework.lang.Nullable; +import org.springframework.util.CollectionUtils; + +import java.lang.reflect.Array; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 集合工具类 + * + * @author L.cm + */ +public class CollectionUtil extends CollectionUtils { + + /** + * Return {@code true} if the supplied Collection is not {@code null} or empty. + * Otherwise, return {@code false}. + * + * @param collection the Collection to check + * @return whether the given Collection is not empty + */ + public static boolean isNotEmpty(@Nullable Collection collection) { + return !CollectionUtil.isEmpty(collection); + } + + /** + * Return {@code true} if the supplied Map is not {@code null} or empty. + * Otherwise, return {@code false}. + * + * @param map the Map to check + * @return whether the given Map is not empty + */ + public static boolean isNotEmpty(@Nullable Map map) { + return !CollectionUtil.isEmpty(map); + } + + /** + * Check whether the given Array contains the given element. + * + * @param array the Array to check + * @param element the element to look for + * @param The generic tag + * @return {@code true} if found, {@code false} else + */ + public static boolean contains(@Nullable T[] array, final T element) { + if (array == null) { + return false; + } + return Arrays.stream(array).anyMatch(x -> ObjectUtil.nullSafeEquals(x, element)); + } + + /** + * Concatenates 2 arrays + * + * @param one 数组1 + * @param other 数组2 + * @return 新数组 + */ + public static String[] concat(String[] one, String[] other) { + return concat(one, other, String.class); + } + + /** + * Concatenates 2 arrays + * + * @param one 数组1 + * @param other 数组2 + * @param clazz 数组类 + * @return 新数组 + */ + public static T[] concat(T[] one, T[] other, Class clazz) { + T[] target = (T[]) Array.newInstance(clazz, one.length + other.length); + System.arraycopy(one, 0, target, 0, one.length); + System.arraycopy(other, 0, target, one.length, other.length); + return target; + } + + /** + * 对象是否为数组对象 + * + * @param obj 对象 + * @return 是否为数组对象,如果为{@code null} 返回false + */ + public static boolean isArray(Object obj) { + if (null == obj) { + return false; + } + return obj.getClass().isArray(); + } + + /** + * 不可变 Set + * + * @param es 对象 + * @param 泛型 + * @return 集合 + */ + @SafeVarargs + public static Set ofImmutableSet(E... es) { + Objects.requireNonNull(es, "args es is null."); + return Arrays.stream(es).collect(Collectors.toSet()); + } + + /** + * 不可变 List + * + * @param es 对象 + * @param 泛型 + * @return 集合 + */ + @SafeVarargs + public static List ofImmutableList(E... es) { + Objects.requireNonNull(es, "args es is null."); + return Arrays.stream(es).collect(Collectors.toList()); + } + + /** + * Iterable 转换为List集合 + * + * @param elements Iterable + * @param 泛型 + * @return 集合 + */ + public static List toList(Iterable elements) { + Objects.requireNonNull(elements, "elements es is null."); + if (elements instanceof Collection) { + return new ArrayList((Collection) elements); + } + Iterator iterator = elements.iterator(); + List list = new ArrayList<>(); + while (iterator.hasNext()) { + list.add(iterator.next()); + } + return list; + } + + /** + * 将key value 数组转为 map + * + * @param keysValues key value 数组 + * @param key + * @param value + * @return map 集合 + */ + public static Map toMap(Object... keysValues) { + int kvLength = keysValues.length; + if (kvLength % 2 != 0) { + throw new IllegalArgumentException("wrong number of arguments for met, keysValues length can not be odd"); + } + Map keyValueMap = new HashMap<>(kvLength); + for (int i = kvLength - 2; i >= 0; i -= 2) { + Object key = keysValues[i]; + Object value = keysValues[i + 1]; + keyValueMap.put((K) key, (V) value); + } + return keyValueMap; + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/ConcurrentDateFormat.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/ConcurrentDateFormat.java new file mode 100644 index 0000000..0a5e28f --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/ConcurrentDateFormat.java @@ -0,0 +1,96 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.tool.utils; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.Queue; +import java.util.TimeZone; +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * 参考tomcat8中的并发DateFormat + *

+ * {@link SimpleDateFormat}的线程安全包装器。 + * 不使用ThreadLocal,创建足够的SimpleDateFormat对象来满足并发性要求。 + *

+ * + * @author L.cm + */ +public class ConcurrentDateFormat { + private final String format; + private final Locale locale; + private final TimeZone timezone; + private final Queue queue = new ConcurrentLinkedQueue<>(); + + private ConcurrentDateFormat(String format, Locale locale, TimeZone timezone) { + this.format = format; + this.locale = locale; + this.timezone = timezone; + SimpleDateFormat initial = createInstance(); + queue.add(initial); + } + + public static ConcurrentDateFormat of(String format) { + return new ConcurrentDateFormat(format, Locale.getDefault(), TimeZone.getDefault()); + } + + public static ConcurrentDateFormat of(String format, TimeZone timezone) { + return new ConcurrentDateFormat(format, Locale.getDefault(), timezone); + } + + public static ConcurrentDateFormat of(String format, Locale locale, TimeZone timezone) { + return new ConcurrentDateFormat(format, locale, timezone); + } + + public String format(Date date) { + SimpleDateFormat sdf = queue.poll(); + if (sdf == null) { + sdf = createInstance(); + } + String result = sdf.format(date); + queue.add(sdf); + return result; + } + + public Date parse(String source) throws ParseException { + SimpleDateFormat sdf = queue.poll(); + if (sdf == null) { + sdf = createInstance(); + } + Date result = sdf.parse(source); + queue.add(sdf); + return result; + } + + private SimpleDateFormat createInstance() { + SimpleDateFormat sdf = new SimpleDateFormat(format, locale); + sdf.setTimeZone(timezone); + return sdf; + } +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/ConvertUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/ConvertUtil.java new file mode 100644 index 0000000..2772560 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/ConvertUtil.java @@ -0,0 +1,81 @@ +package org.springblade.core.tool.utils; + +import org.springblade.core.tool.convert.BladeConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.lang.Nullable; + +/** + * 基于 spring ConversionService 类型转换 + * + * @author L.cm + */ +@SuppressWarnings("unchecked") +public class ConvertUtil { + + /** + * Convenience operation for converting a source object to the specified targetType. + * {@link TypeDescriptor#forObject(Object)}. + * @param source the source object + * @param targetType the target type + * @param 泛型标记 + * @return the converted value + * @throws IllegalArgumentException if targetType is {@code null}, + * or sourceType is {@code null} but source is not {@code null} + */ + @Nullable + public static T convert(@Nullable Object source, Class targetType) { + if (source == null) { + return null; + } + if (ClassUtil.isAssignableValue(targetType, source)) { + return (T) source; + } + GenericConversionService conversionService = BladeConversionService.getInstance(); + return conversionService.convert(source, targetType); + } + + /** + * Convenience operation for converting a source object to the specified targetType, + * where the target type is a descriptor that provides additional conversion context. + * {@link TypeDescriptor#forObject(Object)}. + * @param source the source object + * @param sourceType the source type + * @param targetType the target type + * @param 泛型标记 + * @return the converted value + * @throws IllegalArgumentException if targetType is {@code null}, + * or sourceType is {@code null} but source is not {@code null} + */ + @Nullable + public static T convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (source == null) { + return null; + } + GenericConversionService conversionService = BladeConversionService.getInstance(); + return (T) conversionService.convert(source, sourceType, targetType); + } + + /** + * Convenience operation for converting a source object to the specified targetType, + * where the target type is a descriptor that provides additional conversion context. + * Simply delegates to {@link #convert(Object, TypeDescriptor, TypeDescriptor)} and + * encapsulates the construction of the source type descriptor using + * {@link TypeDescriptor#forObject(Object)}. + * @param source the source object + * @param targetType the target type + * @param 泛型标记 + * @return the converted value + * @throws IllegalArgumentException if targetType is {@code null}, + * or sourceType is {@code null} but source is not {@code null} + */ + @Nullable + public static T convert(@Nullable Object source, TypeDescriptor targetType) { + if (source == null) { + return null; + } + GenericConversionService conversionService = BladeConversionService.getInstance(); + return (T) conversionService.convert(source, targetType); + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/DatatypeConverterUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/DatatypeConverterUtil.java new file mode 100644 index 0000000..1b88ace --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/DatatypeConverterUtil.java @@ -0,0 +1,57 @@ +package org.springblade.core.tool.utils; + +/** + * 数据类型转换工具类 + * + * @author Chill + */ +public class DatatypeConverterUtil { + + /** + * hex文本转换为二进制 + * + * @param hexStr hex文本 + * @return byte[] + */ + public static byte[] parseHexBinary(String hexStr) { + final int len = hexStr.length(); + + if (len % 2 != 0) { + throw new IllegalArgumentException("hexBinary needs to be even-length: " + hexStr); + } + + byte[] out = new byte[len / 2]; + + for (int i = 0; i < len; i += 2) { + int h = hexToBin(hexStr.charAt(i)); + int l = hexToBin(hexStr.charAt(i + 1)); + if (h == -1 || l == -1) { + throw new IllegalArgumentException("contains illegal character for hexBinary: " + hexStr); + } + + out[i / 2] = (byte) (h * 16 + l); + } + + return out; + } + + /** + * hex文本转换为int + * + * @param ch hex文本 + * @return int + */ + private static int hexToBin(char ch) { + if ('0' <= ch && ch <= '9') { + return ch - '0'; + } + if ('A' <= ch && ch <= 'F') { + return ch - 'A' + 10; + } + if ('a' <= ch && ch <= 'f') { + return ch - 'a' + 10; + } + return -1; + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/DateTimeUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/DateTimeUtil.java new file mode 100644 index 0000000..c0d0fad --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/DateTimeUtil.java @@ -0,0 +1,235 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.tool.utils; + +import java.time.*; +import java.time.format.DateTimeFormatter; +import java.time.temporal.Temporal; +import java.time.temporal.TemporalAccessor; +import java.util.Date; + +/** + * DateTime 工具类 + * + * @author L.cm + */ +public class DateTimeUtil { + public static final DateTimeFormatter DATETIME_FORMAT = DateTimeFormatter.ofPattern(DateUtil.PATTERN_DATETIME); + public static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern(DateUtil.PATTERN_DATE); + public static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ofPattern(DateUtil.PATTERN_TIME); + + /** + * 日期时间格式化 + * + * @param temporal 时间 + * @return 格式化后的时间 + */ + public static String formatDateTime(TemporalAccessor temporal) { + return DATETIME_FORMAT.format(temporal); + } + + /** + * 日期时间格式化 + * + * @param temporal 时间 + * @return 格式化后的时间 + */ + public static String formatDate(TemporalAccessor temporal) { + return DATE_FORMAT.format(temporal); + } + + /** + * 时间格式化 + * + * @param temporal 时间 + * @return 格式化后的时间 + */ + public static String formatTime(TemporalAccessor temporal) { + return TIME_FORMAT.format(temporal); + } + + /** + * 日期格式化 + * + * @param temporal 时间 + * @param pattern 表达式 + * @return 格式化后的时间 + */ + public static String format(TemporalAccessor temporal, String pattern) { + return DateTimeFormatter.ofPattern(pattern).format(temporal); + } + + /** + * 将字符串转换为时间 + * + * @param dateStr 时间字符串 + * @param pattern 表达式 + * @return 时间 + */ + public static LocalDateTime parseDateTime(String dateStr, String pattern) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern); + return DateTimeUtil.parseDateTime(dateStr, formatter); + } + + /** + * 将字符串转换为时间 + * + * @param dateStr 时间字符串 + * @param formatter DateTimeFormatter + * @return 时间 + */ + public static LocalDateTime parseDateTime(String dateStr, DateTimeFormatter formatter) { + return LocalDateTime.parse(dateStr, formatter); + } + + /** + * 将字符串转换为时间 + * + * @param dateStr 时间字符串 + * @return 时间 + */ + public static LocalDateTime parseDateTime(String dateStr) { + return DateTimeUtil.parseDateTime(dateStr, DateTimeUtil.DATETIME_FORMAT); + } + + /** + * 将字符串转换为时间 + * + * @param dateStr 时间字符串 + * @param pattern 表达式 + * @return 时间 + */ + public static LocalDate parseDate(String dateStr, String pattern) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern); + return DateTimeUtil.parseDate(dateStr, formatter); + } + + /** + * 将字符串转换为时间 + * + * @param dateStr 时间字符串 + * @param formatter DateTimeFormatter + * @return 时间 + */ + public static LocalDate parseDate(String dateStr, DateTimeFormatter formatter) { + return LocalDate.parse(dateStr, formatter); + } + + /** + * 将字符串转换为日期 + * + * @param dateStr 时间字符串 + * @return 时间 + */ + public static LocalDate parseDate(String dateStr) { + return DateTimeUtil.parseDate(dateStr, DateTimeUtil.DATE_FORMAT); + } + + /** + * 将字符串转换为时间 + * + * @param dateStr 时间字符串 + * @param pattern 时间正则 + * @return 时间 + */ + public static LocalTime parseTime(String dateStr, String pattern) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern); + return DateTimeUtil.parseTime(dateStr, formatter); + } + + /** + * 将字符串转换为时间 + * + * @param dateStr 时间字符串 + * @param formatter DateTimeFormatter + * @return 时间 + */ + public static LocalTime parseTime(String dateStr, DateTimeFormatter formatter) { + return LocalTime.parse(dateStr, formatter); + } + + /** + * 将字符串转换为时间 + * + * @param dateStr 时间字符串 + * @return 时间 + */ + public static LocalTime parseTime(String dateStr) { + return DateTimeUtil.parseTime(dateStr, DateTimeUtil.TIME_FORMAT); + } + + /** + * 时间转 Instant + * + * @param dateTime 时间 + * @return Instant + */ + public static Instant toInstant(LocalDateTime dateTime) { + return dateTime.atZone(ZoneId.systemDefault()).toInstant(); + } + + /** + * Instant 转 时间 + * + * @param instant Instant + * @return Instant + */ + public static LocalDateTime toDateTime(Instant instant) { + return LocalDateTime.ofInstant(instant, ZoneId.systemDefault()); + } + + /** + * 转换成 date + * + * @param dateTime LocalDateTime + * @return Date + */ + public static Date toDate(LocalDateTime dateTime) { + return Date.from(DateTimeUtil.toInstant(dateTime)); + } + + /** + * 比较2个时间差,跨度比较小 + * + * @param startInclusive 开始时间 + * @param endExclusive 结束时间 + * @return 时间间隔 + */ + public static Duration between(Temporal startInclusive, Temporal endExclusive) { + return Duration.between(startInclusive, endExclusive); + } + + /** + * 比较2个时间差,跨度比较大,年月日为单位 + * + * @param startDate 开始时间 + * @param endDate 结束时间 + * @return 时间间隔 + */ + public static Period between(LocalDate startDate, LocalDate endDate) { + return Period.between(startDate, endDate); + } +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/DateUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/DateUtil.java new file mode 100644 index 0000000..0a34c08 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/DateUtil.java @@ -0,0 +1,634 @@ +package org.springblade.core.tool.utils; + +import org.springframework.util.Assert; + +import java.text.ParseException; +import java.time.*; +import java.time.format.DateTimeFormatter; +import java.time.temporal.Temporal; +import java.time.temporal.TemporalAccessor; +import java.time.temporal.TemporalAmount; +import java.time.temporal.TemporalQuery; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.TimeZone; + +/** + * 日期工具类 + * + * @author L.cm + */ +public class DateUtil { + + public static final String PATTERN_DATETIME = "yyyy-MM-dd HH:mm:ss"; + public static final String PATTERN_DATETIME_MINI = "yyyyMMddHHmmss"; + public static final String PATTERN_DATE = "yyyy-MM-dd"; + public static final String PATTERN_TIME = "HH:mm:ss"; + /** + * 老 date 格式化 + */ + public static final ConcurrentDateFormat DATETIME_FORMAT = ConcurrentDateFormat.of(PATTERN_DATETIME); + public static final ConcurrentDateFormat DATETIME_MINI_FORMAT = ConcurrentDateFormat.of(PATTERN_DATETIME_MINI); + public static final ConcurrentDateFormat DATE_FORMAT = ConcurrentDateFormat.of(PATTERN_DATE); + public static final ConcurrentDateFormat TIME_FORMAT = ConcurrentDateFormat.of(PATTERN_TIME); + /** + * java 8 时间格式化 + */ + public static final DateTimeFormatter DATETIME_FORMATTER = DateTimeFormatter.ofPattern(DateUtil.PATTERN_DATETIME); + public static final DateTimeFormatter DATETIME_MINI_FORMATTER = DateTimeFormatter.ofPattern(DateUtil.PATTERN_DATETIME_MINI); + public static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern(DateUtil.PATTERN_DATE); + public static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern(DateUtil.PATTERN_TIME); + + /** + * 获取当前日期 + * + * @return 当前日期 + */ + public static Date now() { + return new Date(); + } + + /** + * 添加年 + * + * @param date 时间 + * @param yearsToAdd 添加的年数 + * @return 设置后的时间 + */ + public static Date plusYears(Date date, int yearsToAdd) { + return DateUtil.set(date, Calendar.YEAR, yearsToAdd); + } + + /** + * 添加月 + * + * @param date 时间 + * @param monthsToAdd 添加的月数 + * @return 设置后的时间 + */ + public static Date plusMonths(Date date, int monthsToAdd) { + return DateUtil.set(date, Calendar.MONTH, monthsToAdd); + } + + /** + * 添加周 + * + * @param date 时间 + * @param weeksToAdd 添加的周数 + * @return 设置后的时间 + */ + public static Date plusWeeks(Date date, int weeksToAdd) { + return DateUtil.plus(date, Period.ofWeeks(weeksToAdd)); + } + + /** + * 添加天 + * + * @param date 时间 + * @param daysToAdd 添加的天数 + * @return 设置后的时间 + */ + public static Date plusDays(Date date, long daysToAdd) { + return DateUtil.plus(date, Duration.ofDays(daysToAdd)); + } + + /** + * 添加小时 + * + * @param date 时间 + * @param hoursToAdd 添加的小时数 + * @return 设置后的时间 + */ + public static Date plusHours(Date date, long hoursToAdd) { + return DateUtil.plus(date, Duration.ofHours(hoursToAdd)); + } + + /** + * 添加分钟 + * + * @param date 时间 + * @param minutesToAdd 添加的分钟数 + * @return 设置后的时间 + */ + public static Date plusMinutes(Date date, long minutesToAdd) { + return DateUtil.plus(date, Duration.ofMinutes(minutesToAdd)); + } + + /** + * 添加秒 + * + * @param date 时间 + * @param secondsToAdd 添加的秒数 + * @return 设置后的时间 + */ + public static Date plusSeconds(Date date, long secondsToAdd) { + return DateUtil.plus(date, Duration.ofSeconds(secondsToAdd)); + } + + /** + * 添加毫秒 + * + * @param date 时间 + * @param millisToAdd 添加的毫秒数 + * @return 设置后的时间 + */ + public static Date plusMillis(Date date, long millisToAdd) { + return DateUtil.plus(date, Duration.ofMillis(millisToAdd)); + } + + /** + * 添加纳秒 + * + * @param date 时间 + * @param nanosToAdd 添加的纳秒数 + * @return 设置后的时间 + */ + public static Date plusNanos(Date date, long nanosToAdd) { + return DateUtil.plus(date, Duration.ofNanos(nanosToAdd)); + } + + /** + * 日期添加时间量 + * + * @param date 时间 + * @param amount 时间量 + * @return 设置后的时间 + */ + public static Date plus(Date date, TemporalAmount amount) { + Instant instant = date.toInstant(); + return Date.from(instant.plus(amount)); + } + + /** + * 减少年 + * + * @param date 时间 + * @param years 减少的年数 + * @return 设置后的时间 + */ + public static Date minusYears(Date date, int years) { + return DateUtil.set(date, Calendar.YEAR, -years); + } + + /** + * 减少月 + * + * @param date 时间 + * @param months 减少的月数 + * @return 设置后的时间 + */ + public static Date minusMonths(Date date, int months) { + return DateUtil.set(date, Calendar.MONTH, -months); + } + + /** + * 减少周 + * + * @param date 时间 + * @param weeks 减少的周数 + * @return 设置后的时间 + */ + public static Date minusWeeks(Date date, int weeks) { + return DateUtil.minus(date, Period.ofWeeks(weeks)); + } + + /** + * 减少天 + * + * @param date 时间 + * @param days 减少的天数 + * @return 设置后的时间 + */ + public static Date minusDays(Date date, long days) { + return DateUtil.minus(date, Duration.ofDays(days)); + } + + /** + * 减少小时 + * + * @param date 时间 + * @param hours 减少的小时数 + * @return 设置后的时间 + */ + public static Date minusHours(Date date, long hours) { + return DateUtil.minus(date, Duration.ofHours(hours)); + } + + /** + * 减少分钟 + * + * @param date 时间 + * @param minutes 减少的分钟数 + * @return 设置后的时间 + */ + public static Date minusMinutes(Date date, long minutes) { + return DateUtil.minus(date, Duration.ofMinutes(minutes)); + } + + /** + * 减少秒 + * + * @param date 时间 + * @param seconds 减少的秒数 + * @return 设置后的时间 + */ + public static Date minusSeconds(Date date, long seconds) { + return DateUtil.minus(date, Duration.ofSeconds(seconds)); + } + + /** + * 减少毫秒 + * + * @param date 时间 + * @param millis 减少的毫秒数 + * @return 设置后的时间 + */ + public static Date minusMillis(Date date, long millis) { + return DateUtil.minus(date, Duration.ofMillis(millis)); + } + + /** + * 减少纳秒 + * + * @param date 时间 + * @param nanos 减少的纳秒数 + * @return 设置后的时间 + */ + public static Date minusNanos(Date date, long nanos) { + return DateUtil.minus(date, Duration.ofNanos(nanos)); + } + + /** + * 日期减少时间量 + * + * @param date 时间 + * @param amount 时间量 + * @return 设置后的时间 + */ + public static Date minus(Date date, TemporalAmount amount) { + Instant instant = date.toInstant(); + return Date.from(instant.minus(amount)); + } + + /** + * 设置日期属性 + * + * @param date 时间 + * @param calendarField 更改的属性 + * @param amount 更改数,-1表示减少 + * @return 设置后的时间 + */ + private static Date set(Date date, int calendarField, int amount) { + Assert.notNull(date, "The date must not be null"); + Calendar c = Calendar.getInstance(); + c.setLenient(false); + c.setTime(date); + c.add(calendarField, amount); + return c.getTime(); + } + + /** + * 日期时间格式化 + * + * @param date 时间 + * @return 格式化后的时间 + */ + public static String formatDateTime(Date date) { + return DATETIME_FORMAT.format(date); + } + + /** + * 日期时间格式化 + * + * @param date 时间 + * @return 格式化后的时间 + */ + public static String formatDateTimeMini(Date date) { + return DATETIME_MINI_FORMAT.format(date); + } + + /** + * 日期格式化 + * + * @param date 时间 + * @return 格式化后的时间 + */ + public static String formatDate(Date date) { + return DATE_FORMAT.format(date); + } + + /** + * 时间格式化 + * + * @param date 时间 + * @return 格式化后的时间 + */ + public static String formatTime(Date date) { + return TIME_FORMAT.format(date); + } + + /** + * 日期格式化 + * + * @param date 时间 + * @param pattern 表达式 + * @return 格式化后的时间 + */ + public static String format(Date date, String pattern) { + return ConcurrentDateFormat.of(pattern).format(date); + } + + /** + * java8 日期时间格式化 + * + * @param temporal 时间 + * @return 格式化后的时间 + */ + public static String formatDateTime(TemporalAccessor temporal) { + return DATETIME_FORMATTER.format(temporal); + } + + /** + * java8 日期时间格式化 + * + * @param temporal 时间 + * @return 格式化后的时间 + */ + public static String formatDateTimeMini(TemporalAccessor temporal) { + return DATETIME_MINI_FORMATTER.format(temporal); + } + + /** + * java8 日期时间格式化 + * + * @param temporal 时间 + * @return 格式化后的时间 + */ + public static String formatDate(TemporalAccessor temporal) { + return DATE_FORMATTER.format(temporal); + } + + /** + * java8 时间格式化 + * + * @param temporal 时间 + * @return 格式化后的时间 + */ + public static String formatTime(TemporalAccessor temporal) { + return TIME_FORMATTER.format(temporal); + } + + /** + * java8 日期格式化 + * + * @param temporal 时间 + * @param pattern 表达式 + * @return 格式化后的时间 + */ + public static String format(TemporalAccessor temporal, String pattern) { + return DateTimeFormatter.ofPattern(pattern).format(temporal); + } + + /** + * 将字符串转换为时间 + * + * @param dateStr 时间字符串 + * @param pattern 表达式 + * @return 时间 + */ + public static Date parse(String dateStr, String pattern) { + ConcurrentDateFormat format = ConcurrentDateFormat.of(pattern); + try { + return format.parse(dateStr); + } catch (ParseException e) { + throw Exceptions.unchecked(e); + } + } + + /** + * 将字符串转换为时间 + * + * @param dateStr 时间字符串 + * @param format ConcurrentDateFormat + * @return 时间 + */ + public static Date parse(String dateStr, ConcurrentDateFormat format) { + try { + return format.parse(dateStr); + } catch (ParseException e) { + throw Exceptions.unchecked(e); + } + } + + /** + * 将字符串转换为时间 + * + * @param dateStr 时间字符串 + * @param pattern 表达式 + * @return 时间 + */ + public static T parse(String dateStr, String pattern, TemporalQuery query) { + return DateTimeFormatter.ofPattern(pattern).parse(dateStr, query); + } + + /** + * 时间转 Instant + * + * @param dateTime 时间 + * @return Instant + */ + public static Instant toInstant(LocalDateTime dateTime) { + return dateTime.atZone(ZoneId.systemDefault()).toInstant(); + } + + /** + * Instant 转 时间 + * + * @param instant Instant + * @return Instant + */ + public static LocalDateTime toDateTime(Instant instant) { + return LocalDateTime.ofInstant(instant, ZoneId.systemDefault()); + } + + /** + * 转换成 date + * + * @param dateTime LocalDateTime + * @return Date + */ + public static Date toDate(LocalDateTime dateTime) { + return Date.from(DateUtil.toInstant(dateTime)); + } + + /** + * 转换成 date + * + * @param localDate LocalDate + * @return Date + */ + public static Date toDate(final LocalDate localDate) { + return Date.from(localDate.atStartOfDay(ZoneId.systemDefault()).toInstant()); + } + + /** + * Converts local date time to Calendar. + */ + public static Calendar toCalendar(final LocalDateTime localDateTime) { + return GregorianCalendar.from(ZonedDateTime.of(localDateTime, ZoneId.systemDefault())); + } + + /** + * localDateTime 转换成毫秒数 + * + * @param localDateTime LocalDateTime + * @return long + */ + public static long toMilliseconds(final LocalDateTime localDateTime) { + return localDateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(); + } + + /** + * localDate 转换成毫秒数 + * + * @param localDate LocalDate + * @return long + */ + public static long toMilliseconds(LocalDate localDate) { + return toMilliseconds(localDate.atStartOfDay()); + } + + /** + * 转换成java8 时间 + * + * @param calendar 日历 + * @return LocalDateTime + */ + public static LocalDateTime fromCalendar(final Calendar calendar) { + TimeZone tz = calendar.getTimeZone(); + ZoneId zid = tz == null ? ZoneId.systemDefault() : tz.toZoneId(); + return LocalDateTime.ofInstant(calendar.toInstant(), zid); + } + + /** + * 转换成java8 时间 + * + * @param instant Instant + * @return LocalDateTime + */ + public static LocalDateTime fromInstant(final Instant instant) { + return LocalDateTime.ofInstant(instant, ZoneId.systemDefault()); + } + + /** + * 转换成java8 时间 + * + * @param date Date + * @return LocalDateTime + */ + public static LocalDateTime fromDate(final Date date) { + return LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault()); + } + + /** + * 转换成java8 时间 + * + * @param milliseconds 毫秒数 + * @return LocalDateTime + */ + public static LocalDateTime fromMilliseconds(final long milliseconds) { + return LocalDateTime.ofInstant(Instant.ofEpochMilli(milliseconds), ZoneId.systemDefault()); + } + + /** + * 比较2个时间差,跨度比较小 + * + * @param startInclusive 开始时间 + * @param endExclusive 结束时间 + * @return 时间间隔 + */ + public static Duration between(Temporal startInclusive, Temporal endExclusive) { + return Duration.between(startInclusive, endExclusive); + } + + /** + * 比较2个时间差,跨度比较大,年月日为单位 + * + * @param startDate 开始时间 + * @param endDate 结束时间 + * @return 时间间隔 + */ + public static Period between(LocalDate startDate, LocalDate endDate) { + return Period.between(startDate, endDate); + } + + /** + * 比较2个 时间差 + * + * @param startDate 开始时间 + * @param endDate 结束时间 + * @return 时间间隔 + */ + public static Duration between(Date startDate, Date endDate) { + return Duration.between(startDate.toInstant(), endDate.toInstant()); + } + + /** + * 将秒数转换为日时分秒 + * + * @param second 秒数 + * @return 时间 + */ + public static String secondToTime(Long second) { + // 判断是否为空 + if (second == null || second == 0L) { + return StringPool.EMPTY; + } + //转换天数 + long days = second / 86400; + //剩余秒数 + second = second % 86400; + //转换小时 + long hours = second / 3600; + //剩余秒数 + second = second % 3600; + //转换分钟 + long minutes = second / 60; + //剩余秒数 + second = second % 60; + if (days > 0) { + return StringUtil.format("{}天{}小时{}分{}秒", days, hours, minutes, second); + } else { + return StringUtil.format("{}小时{}分{}秒", hours, minutes, second); + } + } + + /** + * 获取今天的日期 + * + * @return 时间 + */ + public static String today() { + return format(new Date(), "yyyyMMdd"); + } + + /** + * 获取今天的时间 + * + * @return 时间 + */ + public static String time() { + return format(new Date(), PATTERN_DATETIME_MINI); + } + + /** + * 获取今天的小时数 + * + * @return 时间 + */ + public static Integer hour() { + return NumberUtil.toInt(format(new Date(), "HH")); + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/DesUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/DesUtil.java new file mode 100644 index 0000000..fe8f574 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/DesUtil.java @@ -0,0 +1,217 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.tool.utils; + +import org.springframework.lang.Nullable; + +import javax.crypto.Cipher; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.DESKeySpec; +import java.util.Objects; + +/** + * DES加解密处理工具 + * + * @author L.cm + */ +public class DesUtil { + /** + * 数字签名,密钥算法 + */ + public static final String DES_ALGORITHM = "DES"; + + /** + * 生成 des 密钥 + * + * @return 密钥 + */ + public static String genDesKey() { + return StringUtil.random(16); + } + + /** + * DES加密 + * + * @param data byte array + * @param password 密钥 + * @return des hex + */ + public static String encryptToHex(byte[] data, String password) { + return HexUtil.encodeToString(encrypt(data, password)); + } + + /** + * DES加密 + * + * @param data 字符串内容 + * @param password 密钥 + * @return des hex + */ + @Nullable + public static String encryptToHex(@Nullable String data, String password) { + if (StringUtil.isBlank(data)) { + return null; + } + byte[] dataBytes = data.getBytes(Charsets.UTF_8); + return encryptToHex(dataBytes, password); + } + + /** + * DES解密 + * + * @param data 字符串内容 + * @param password 密钥 + * @return des context + */ + @Nullable + public static String decryptFormHex(@Nullable String data, String password) { + if (StringUtil.isBlank(data)) { + return null; + } + byte[] hexBytes = HexUtil.decode(data); + return new String(decrypt(hexBytes, password), Charsets.UTF_8); + } + + /** + * DES加密 + * + * @param data byte array + * @param password 密钥 + * @return des hex + */ + public static String encryptToBase64(byte[] data, String password) { + return Base64Util.encodeToString(encrypt(data, password)); + } + + /** + * DES加密 + * + * @param data 字符串内容 + * @param password 密钥 + * @return des hex + */ + @Nullable + public static String encryptToBase64(@Nullable String data, String password) { + if (StringUtil.isBlank(data)) { + return null; + } + byte[] dataBytes = data.getBytes(Charsets.UTF_8); + return encryptToBase64(dataBytes, password); + } + + /** + * DES解密 + * + * @param data 字符串内容 + * @param password 密钥 + * @return des context + */ + public static byte[] decryptFormBase64(byte[] data, String password) { + byte[] dataBytes = Base64Util.decode(data); + return decrypt(dataBytes, password); + } + + /** + * DES解密 + * + * @param data 字符串内容 + * @param password 密钥 + * @return des context + */ + @Nullable + public static String decryptFormBase64(@Nullable String data, String password) { + if (StringUtil.isBlank(data)) { + return null; + } + byte[] dataBytes = Base64Util.decodeFromString(data); + return new String(decrypt(dataBytes, password), Charsets.UTF_8); + } + + /** + * DES加密 + * + * @param data 内容 + * @param desKey 密钥 + * @return byte array + */ + public static byte[] encrypt(byte[] data, byte[] desKey) { + return des(data, desKey, Cipher.ENCRYPT_MODE); + } + + /** + * DES加密 + * + * @param data 内容 + * @param desKey 密钥 + * @return byte array + */ + public static byte[] encrypt(byte[] data, String desKey) { + return encrypt(data, Objects.requireNonNull(desKey).getBytes(Charsets.UTF_8)); + } + + /** + * DES解密 + * + * @param data 内容 + * @param desKey 密钥 + * @return byte array + */ + public static byte[] decrypt(byte[] data, byte[] desKey) { + return des(data, desKey, Cipher.DECRYPT_MODE); + } + + /** + * DES解密 + * + * @param data 内容 + * @param desKey 密钥 + * @return byte array + */ + public static byte[] decrypt(byte[] data, String desKey) { + return decrypt(data, Objects.requireNonNull(desKey).getBytes(Charsets.UTF_8)); + } + + /** + * DES加密/解密公共方法 + * + * @param data byte数组 + * @param desKey 密钥 + * @param mode 加密:{@link Cipher#ENCRYPT_MODE},解密:{@link Cipher#DECRYPT_MODE} + * @return des + */ + private static byte[] des(byte[] data, byte[] desKey, int mode) { + try { + SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(DES_ALGORITHM); + Cipher cipher = Cipher.getInstance(DES_ALGORITHM); + DESKeySpec desKeySpec = new DESKeySpec(desKey); + cipher.init(mode, keyFactory.generateSecret(desKeySpec), Holder.SECURE_RANDOM); + return cipher.doFinal(data); + } catch (Exception e) { + throw Exceptions.unchecked(e); + } + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/DigestUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/DigestUtil.java new file mode 100644 index 0000000..30bca89 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/DigestUtil.java @@ -0,0 +1,459 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.tool.utils; + +import org.springframework.lang.Nullable; +import org.springframework.util.DigestUtils; + +import javax.crypto.Mac; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * 加密相关工具类直接使用Spring util封装,减少jar依赖 + * + * @author L.cm + */ +public class DigestUtil extends org.springframework.util.DigestUtils { + private static final char[] HEX_CODE = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; + + /** + * Calculates the MD5 digest and returns the value as a 32 character hex string. + * + * @param data Data to digest + * @return MD5 digest as a hex string + */ + public static String md5Hex(final String data) { + return DigestUtils.md5DigestAsHex(data.getBytes(Charsets.UTF_8)); + } + + /** + * Return a hexadecimal string representation of the MD5 digest of the given bytes. + * + * @param bytes the bytes to calculate the digest over + * @return a hexadecimal digest string + */ + public static String md5Hex(final byte[] bytes) { + return DigestUtils.md5DigestAsHex(bytes); + } + + /** + * sha1Hex + * + * @param data Data to digest + * @return digest as a hex string + */ + public static String sha1Hex(String data) { + return DigestUtil.sha1Hex(data.getBytes(Charsets.UTF_8)); + } + + /** + * sha1Hex + * + * @param bytes Data to digest + * @return digest as a hex string + */ + public static String sha1Hex(final byte[] bytes) { + return DigestUtil.digestHex("SHA-1", bytes); + } + + /** + * SHA224Hex + * + * @param data Data to digest + * @return digest as a hex string + */ + public static String sha224Hex(String data) { + return DigestUtil.sha224Hex(data.getBytes(Charsets.UTF_8)); + } + + /** + * SHA224Hex + * + * @param bytes Data to digest + * @return digest as a hex string + */ + public static String sha224Hex(final byte[] bytes) { + return DigestUtil.digestHex("SHA-224", bytes); + } + + /** + * sha256Hex + * + * @param data Data to digest + * @return digest as a hex string + */ + public static String sha256Hex(String data) { + return DigestUtil.sha256Hex(data.getBytes(Charsets.UTF_8)); + } + + /** + * sha256Hex + * + * @param bytes Data to digest + * @return digest as a hex string + */ + public static String sha256Hex(final byte[] bytes) { + return DigestUtil.digestHex("SHA-256", bytes); + } + + /** + * sha384Hex + * + * @param data Data to digest + * @return digest as a hex string + */ + public static String sha384Hex(String data) { + return DigestUtil.sha384Hex(data.getBytes(Charsets.UTF_8)); + } + + /** + * sha384Hex + * + * @param bytes Data to digest + * @return digest as a hex string + */ + public static String sha384Hex(final byte[] bytes) { + return DigestUtil.digestHex("SHA-384", bytes); + } + + /** + * sha512Hex + * + * @param data Data to digest + * @return digest as a hex string + */ + public static String sha512Hex(String data) { + return DigestUtil.sha512Hex(data.getBytes(Charsets.UTF_8)); + } + + /** + * sha512Hex + * + * @param bytes Data to digest + * @return digest as a hex string + */ + public static String sha512Hex(final byte[] bytes) { + return DigestUtil.digestHex("SHA-512", bytes); + } + + /** + * digest Hex + * + * @param algorithm 算法 + * @param bytes Data to digest + * @return digest as a hex string + */ + public static String digestHex(String algorithm, byte[] bytes) { + try { + MessageDigest md = MessageDigest.getInstance(algorithm); + return encodeHex(md.digest(bytes)); + } catch (NoSuchAlgorithmException e) { + throw Exceptions.unchecked(e); + } + } + + /** + * hmacMd5 Hex + * + * @param data Data to digest + * @param key key + * @return digest as a hex string + */ + public static String hmacMd5Hex(String data, String key) { + return DigestUtil.hmacMd5Hex(data.getBytes(Charsets.UTF_8), key); + } + + /** + * hmacMd5 Hex + * + * @param bytes Data to digest + * @param key key + * @return digest as a hex string + */ + public static String hmacMd5Hex(final byte[] bytes, String key) { + return DigestUtil.digestHMacHex("HmacMD5", bytes, key); + } + + /** + * hmacSha1 Hex + * + * @param data Data to digest + * @param key key + * @return digest as a hex string + */ + public static String hmacSha1Hex(String data, String key) { + return DigestUtil.hmacSha1Hex(data.getBytes(Charsets.UTF_8), key); + } + + /** + * hmacSha1 Hex + * + * @param bytes Data to digest + * @param key key + * @return digest as a hex string + */ + public static String hmacSha1Hex(final byte[] bytes, String key) { + return DigestUtil.digestHMacHex("HmacSHA1", bytes, key); + } + + /** + * hmacSha224 Hex + * + * @param data Data to digest + * @param key key + * @return digest as a hex string + */ + public static String hmacSha224Hex(String data, String key) { + return DigestUtil.hmacSha224Hex(data.getBytes(Charsets.UTF_8), key); + } + + /** + * hmacSha224 Hex + * + * @param bytes Data to digest + * @param key key + * @return digest as a hex string + */ + public static String hmacSha224Hex(final byte[] bytes, String key) { + return DigestUtil.digestHMacHex("HmacSHA224", bytes, key); + } + + /** + * hmacSha256 + * + * @param data Data to digest + * @param key key + * @return digest as a hex string + */ + public static byte[] hmacSha256(String data, String key) { + return DigestUtil.hmacSha256(data.getBytes(Charsets.UTF_8), key); + } + + /** + * hmacSha256 + * + * @param bytes Data to digest + * @param key key + * @return digest as a byte array + */ + public static byte[] hmacSha256(final byte[] bytes, String key) { + return DigestUtil.digestHMac("HmacSHA256", bytes, key); + } + + /** + * hmacSha256 Hex + * + * @param data Data to digest + * @param key key + * @return digest as a hex string + */ + public static String hmacSha256Hex(String data, String key) { + return DigestUtil.hmacSha256Hex(data.getBytes(Charsets.UTF_8), key); + } + + /** + * hmacSha256 Hex + * + * @param bytes Data to digest + * @param key key + * @return digest as a hex string + */ + public static String hmacSha256Hex(final byte[] bytes, String key) { + return DigestUtil.digestHMacHex("HmacSHA256", bytes, key); + } + + /** + * hmacSha384 Hex + * + * @param data Data to digest + * @param key key + * @return digest as a hex string + */ + public static String hmacSha384Hex(String data, String key) { + return DigestUtil.hmacSha384Hex(data.getBytes(Charsets.UTF_8), key); + } + + /** + * hmacSha384 Hex + * + * @param bytes Data to digest + * @param key key + * @return digest as a hex string + */ + public static String hmacSha384Hex(final byte[] bytes, String key) { + return DigestUtil.digestHMacHex("HmacSHA384", bytes, key); + } + + /** + * hmacSha512 Hex + * + * @param data Data to digest + * @param key key + * @return digest as a hex string + */ + public static String hmacSha512Hex(String data, String key) { + return DigestUtil.hmacSha512Hex(data.getBytes(Charsets.UTF_8), key); + } + + /** + * hmacSha512 Hex + * + * @param bytes Data to digest + * @param key key + * @return digest as a hex string + */ + public static String hmacSha512Hex(final byte[] bytes, String key) { + return DigestUtil.digestHMacHex("HmacSHA512", bytes, key); + } + + /** + * digest HMac Hex + * + * @param algorithm 算法 + * @param bytes Data to digest + * @return digest as a hex string + */ + public static String digestHMacHex(String algorithm, final byte[] bytes, String key) { + SecretKey secretKey = new SecretKeySpec(key.getBytes(Charsets.UTF_8), algorithm); + try { + Mac mac = Mac.getInstance(secretKey.getAlgorithm()); + mac.init(secretKey); + return DigestUtil.encodeHex(mac.doFinal(bytes)); + } catch (NoSuchAlgorithmException | InvalidKeyException e) { + throw Exceptions.unchecked(e); + } + } + + /** + * digest HMac + * + * @param algorithm 算法 + * @param bytes Data to digest + * @return digest as a byte array + */ + public static byte[] digestHMac(String algorithm, final byte[] bytes, String key) { + SecretKey secretKey = new SecretKeySpec(key.getBytes(Charsets.UTF_8), algorithm); + try { + Mac mac = Mac.getInstance(secretKey.getAlgorithm()); + mac.init(secretKey); + return mac.doFinal(bytes); + } catch (NoSuchAlgorithmException | InvalidKeyException e) { + throw Exceptions.unchecked(e); + } + } + + /** + * encode Hex + * + * @param bytes Data to Hex + * @return bytes as a hex string + */ + public static String encodeHex(byte[] bytes) { + StringBuilder r = new StringBuilder(bytes.length * 2); + for (byte b : bytes) { + r.append(HEX_CODE[(b >> 4) & 0xF]); + r.append(HEX_CODE[(b & 0xF)]); + } + return r.toString(); + } + + /** + * decode Hex + * + * @param hexStr Hex string + * @return decode hex to bytes + */ + public static byte[] decodeHex(final String hexStr) { + return DatatypeConverterUtil.parseHexBinary(hexStr); + } + + /** + * 比较字符串,避免字符串因为过长,产生耗时 + * + * @param a String + * @param b String + * @return 是否相同 + */ + public static boolean slowEquals(@Nullable String a, @Nullable String b) { + if (a == null || b == null) { + return false; + } + return DigestUtil.slowEquals(a.getBytes(Charsets.UTF_8), b.getBytes(Charsets.UTF_8)); + } + + /** + * 比较 byte 数组,避免字符串因为过长,产生耗时 + * + * @param a byte array + * @param b byte array + * @return 是否相同 + */ + public static boolean slowEquals(@Nullable byte[] a, @Nullable byte[] b) { + if (a == null || b == null) { + return false; + } + if (a.length != b.length) { + return false; + } + int diff = a.length ^ b.length; + for (int i = 0; i < a.length; i++) { + diff |= a[i] ^ b[i]; + } + return diff == 0; + } + + /** + * 自定义加密 将前端传递的密码再次加密 + * + * @param data 数据 + * @return {String} + */ + public static String hex(String data) { + if (StringUtil.isBlank(data)) { + return StringPool.EMPTY; + } + return sha1Hex(data); + } + + /** + * 用户密码加密规则 先MD5再SHA1 + * + * @param data 数据 + * @return {String} + */ + public static String encrypt(String data) { + if (StringUtil.isBlank(data)) { + return StringPool.EMPTY; + } + return sha1Hex(md5Hex(data)); + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/Exceptions.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/Exceptions.java new file mode 100644 index 0000000..0170f56 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/Exceptions.java @@ -0,0 +1,108 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.tool.utils; + +import org.springblade.core.tool.support.FastStringWriter; + +import java.io.PrintWriter; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.UndeclaredThrowableException; + +/** + * 异常处理工具类 + * + * @author L.cm + */ +public class Exceptions { + + /** + * 将CheckedException转换为UncheckedException. + * + * @param e Throwable + * @return {RuntimeException} + */ + public static RuntimeException unchecked(Throwable e) { + if (e instanceof Error) { + throw (Error) e; + } else if (e instanceof IllegalAccessException || + e instanceof IllegalArgumentException || + e instanceof NoSuchMethodException) { + return new IllegalArgumentException(e); + } else if (e instanceof InvocationTargetException) { + return new RuntimeException(((InvocationTargetException) e).getTargetException()); + } else if (e instanceof RuntimeException) { + return (RuntimeException) e; + } else if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + return Exceptions.runtime(e); + } + + /** + * 不采用 RuntimeException 包装,直接抛出,使异常更加精准 + * + * @param throwable Throwable + * @param 泛型标记 + * @return Throwable + * @throws T 泛型 + */ + @SuppressWarnings("unchecked") + private static T runtime(Throwable throwable) throws T { + throw (T) throwable; + } + + /** + * 代理异常解包 + * + * @param wrapped 包装过得异常 + * @return 解包后的异常 + */ + public static Throwable unwrap(Throwable wrapped) { + Throwable unwrapped = wrapped; + while (true) { + if (unwrapped instanceof InvocationTargetException) { + unwrapped = ((InvocationTargetException) unwrapped).getTargetException(); + } else if (unwrapped instanceof UndeclaredThrowableException) { + unwrapped = ((UndeclaredThrowableException) unwrapped).getUndeclaredThrowable(); + } else { + return unwrapped; + } + } + } + + /** + * 将ErrorStack转化为String. + * + * @param ex Throwable + * @return {String} + */ + public static String getStackTraceAsString(Throwable ex) { + FastStringWriter stringWriter = new FastStringWriter(); + ex.printStackTrace(new PrintWriter(stringWriter)); + return stringWriter.toString(); + } +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/FileUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/FileUtil.java new file mode 100644 index 0000000..4a9d93c --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/FileUtil.java @@ -0,0 +1,392 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.tool.utils; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.FileSystemUtils; +import org.springframework.util.PatternMatchUtils; +import org.springframework.web.multipart.MultipartFile; + +import java.io.*; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.List; + +/** + * 文件工具类 + * + * @author L.cm + */ +public class FileUtil extends org.springframework.util.FileCopyUtils { + + /** + * 默认为true + * + * @author L.cm + */ + public static class TrueFilter implements FileFilter, Serializable { + private static final long serialVersionUID = -6420452043795072619L; + + public final static TrueFilter TRUE = new TrueFilter(); + + @Override + public boolean accept(File pathname) { + return true; + } + } + + /** + * 扫描目录下的文件 + * + * @param path 路径 + * @return 文件集合 + */ + public static List list(String path) { + File file = new File(path); + return list(file, TrueFilter.TRUE); + } + + /** + * 扫描目录下的文件 + * + * @param path 路径 + * @param fileNamePattern 文件名 * 号 + * @return 文件集合 + */ + public static List list(String path, final String fileNamePattern) { + File file = new File(path); + return list(file, pathname -> { + String fileName = pathname.getName(); + return PatternMatchUtils.simpleMatch(fileNamePattern, fileName); + }); + } + + /** + * 扫描目录下的文件 + * + * @param path 路径 + * @param filter 文件过滤 + * @return 文件集合 + */ + public static List list(String path, FileFilter filter) { + File file = new File(path); + return list(file, filter); + } + + /** + * 扫描目录下的文件 + * + * @param file 文件 + * @return 文件集合 + */ + public static List list(File file) { + List fileList = new ArrayList<>(); + return list(file, fileList, TrueFilter.TRUE); + } + + /** + * 扫描目录下的文件 + * + * @param file 文件 + * @param fileNamePattern Spring AntPathMatcher 规则 + * @return 文件集合 + */ + public static List list(File file, final String fileNamePattern) { + List fileList = new ArrayList<>(); + return list(file, fileList, pathname -> { + String fileName = pathname.getName(); + return PatternMatchUtils.simpleMatch(fileNamePattern, fileName); + }); + } + + /** + * 扫描目录下的文件 + * + * @param file 文件 + * @param filter 文件过滤 + * @return 文件集合 + */ + public static List list(File file, FileFilter filter) { + List fileList = new ArrayList<>(); + return list(file, fileList, filter); + } + + /** + * 扫描目录下的文件 + * + * @param file 文件 + * @param filter 文件过滤 + * @return 文件集合 + */ + private static List list(File file, List fileList, FileFilter filter) { + if (file.isDirectory()) { + File[] files = file.listFiles(); + if (files != null) { + for (File f : files) { + list(f, fileList, filter); + } + } + } else { + // 过滤文件 + boolean accept = filter.accept(file); + if (file.exists() && accept) { + fileList.add(file); + } + } + return fileList; + } + + /** + * 获取文件后缀名 + * @param fullName 文件全名 + * @return {String} + */ + public static String getFileExtension(String fullName) { + if (StringUtil.isBlank(fullName)) return StringPool.EMPTY; + String fileName = new File(fullName).getName(); + int dotIndex = fileName.lastIndexOf(CharPool.DOT); + return (dotIndex == -1) ? "" : fileName.substring(dotIndex + 1); + } + + /** + * 获取文件名,去除后缀名 + * @param fullName 文件全名 + * @return {String} + */ + public static String getNameWithoutExtension(String fullName) { + if (StringUtil.isBlank(fullName)) return StringPool.EMPTY; + String fileName = new File(fullName).getName(); + int dotIndex = fileName.lastIndexOf(CharPool.DOT); + return (dotIndex == -1) ? fileName : fileName.substring(0, dotIndex); + } + + /** + * Returns the path to the system temporary directory. + * + * @return the path to the system temporary directory. + */ + public static String getTempDirPath() { + return System.getProperty("java.io.tmpdir"); + } + + /** + * Returns a {@link File} representing the system temporary directory. + * + * @return the system temporary directory. + */ + public static File getTempDir() { + return new File(getTempDirPath()); + } + + /** + * Reads the contents of a file into a String. + * The file is always closed. + * + * @param file the file to read, must not be {@code null} + * @return the file contents, never {@code null} + */ + public static String readToString(final File file) { + return readToString(file, Charsets.UTF_8); + } + + /** + * Reads the contents of a file into a String. + * The file is always closed. + * + * @param file the file to read, must not be {@code null} + * @param encoding the encoding to use, {@code null} means platform default + * @return the file contents, never {@code null} + */ + public static String readToString(final File file, final Charset encoding) { + try (InputStream in = Files.newInputStream(file.toPath())) { + return IoUtil.readToString(in, encoding); + } catch (IOException e) { + throw Exceptions.unchecked(e); + } + } + + /** + * Reads the contents of a file into a String. + * The file is always closed. + * + * @param file the file to read, must not be {@code null} + * @return the file contents, never {@code null} + */ + public static byte[] readToByteArray(final File file) { + try (InputStream in = Files.newInputStream(file.toPath())) { + return IoUtil.readToByteArray(in); + } catch (IOException e) { + throw Exceptions.unchecked(e); + } + } + + /** + * Writes a String to a file creating the file if it does not exist. + * + * @param file the file to write + * @param data the content to write to the file + */ + public static void writeToFile(final File file, final String data) { + writeToFile(file, data, Charsets.UTF_8, false); + } + + /** + * Writes a String to a file creating the file if it does not exist. + * + * @param file the file to write + * @param data the content to write to the file + * @param append if {@code true}, then the String will be added to the + * end of the file rather than overwriting + */ + public static void writeToFile(final File file, final String data, final boolean append){ + writeToFile(file, data, Charsets.UTF_8, append); + } + + /** + * Writes a String to a file creating the file if it does not exist. + * + * @param file the file to write + * @param data the content to write to the file + * @param encoding the encoding to use, {@code null} means platform default + */ + public static void writeToFile(final File file, final String data, final Charset encoding) { + writeToFile(file, data, encoding, false); + } + + /** + * Writes a String to a file creating the file if it does not exist. + * + * @param file the file to write + * @param data the content to write to the file + * @param encoding the encoding to use, {@code null} means platform default + * @param append if {@code true}, then the String will be added to the + * end of the file rather than overwriting + */ + public static void writeToFile(final File file, final String data, final Charset encoding, final boolean append) { + try (OutputStream out = new FileOutputStream(file, append)) { + IoUtil.write(data, out, encoding); + } catch (IOException e) { + throw Exceptions.unchecked(e); + } + } + + /** + * 转成file + * @param multipartFile MultipartFile + * @param file File + */ + public static void toFile(MultipartFile multipartFile, final File file) { + try { + FileUtil.toFile(multipartFile.getInputStream(), file); + } catch (IOException e) { + throw Exceptions.unchecked(e); + } + } + + /** + * 转成file + * @param in InputStream + * @param file File + */ + public static void toFile(InputStream in, final File file) { + try (OutputStream out = new FileOutputStream(file)) { + FileUtil.copy(in, out); + } catch (IOException e) { + throw Exceptions.unchecked(e); + } + } + + /** + * Moves a file. + *

+ * When the destination file is on another file system, do a "copy and delete". + * + * @param srcFile the file to be moved + * @param destFile the destination file + * @throws NullPointerException if source or destination is {@code null} + * @throws IOException if source or destination is invalid + * @throws IOException if an IO error occurs moving the file + */ + public static void moveFile(final File srcFile, final File destFile) throws IOException { + Assert.notNull(srcFile, "Source must not be null"); + Assert.notNull(destFile, "Destination must not be null"); + if (!srcFile.exists()) { + throw new FileNotFoundException("Source '" + srcFile + "' does not exist"); + } + if (srcFile.isDirectory()) { + throw new IOException("Source '" + srcFile + "' is a directory"); + } + if (destFile.exists()) { + throw new IOException("Destination '" + destFile + "' already exists"); + } + if (destFile.isDirectory()) { + throw new IOException("Destination '" + destFile + "' is a directory"); + } + final boolean rename = srcFile.renameTo(destFile); + if (!rename) { + FileUtil.copy(srcFile, destFile); + if (!srcFile.delete()) { + FileUtil.deleteQuietly(destFile); + throw new IOException("Failed to delete original file '" + srcFile + "' after copy to '" + destFile + "'"); + } + } + } + + /** + * Deletes a file, never throwing an exception. If file is a directory, delete it and all sub-directories. + *

+ * The difference between File.delete() and this method are: + *

    + *
  • A directory to be deleted does not have to be empty.
  • + *
  • No exceptions are thrown when a file or directory cannot be deleted.
  • + *
+ * + * @param file file or directory to delete, can be {@code null} + * @return {@code true} if the file or directory was deleted, otherwise + * {@code false} + */ + public static boolean deleteQuietly(@Nullable final File file) { + if (file == null) { + return false; + } + try { + if (file.isDirectory()) { + FileSystemUtils.deleteRecursively(file); + } + } catch (final Exception ignored) { + } + + try { + return file.delete(); + } catch (final Exception ignored) { + return false; + } + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/Func.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/Func.java new file mode 100644 index 0000000..b2c5471 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/Func.java @@ -0,0 +1,2165 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.tool.utils; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import org.springblade.core.tool.jackson.JsonUtil; +import org.springframework.beans.BeansException; +import org.springframework.core.MethodParameter; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.lang.Nullable; +import org.springframework.util.PatternMatchUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.method.HandlerMethod; + +import java.io.Closeable; +import java.io.File; +import java.io.InputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.nio.charset.Charset; +import java.text.DecimalFormat; +import java.time.Duration; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.Temporal; +import java.time.temporal.TemporalAccessor; +import java.util.*; +import java.util.function.Supplier; + +/** + * 工具包集合,工具类快捷方式 + * + * @author L.cm + */ +public class Func { + + /** + * 断言,必须不能为 null + *

+	 * public Foo(Bar bar) {
+	 *     this.bar = $.requireNotNull(bar);
+	 * }
+	 * 
+ * + * @param obj the object reference to check for nullity + * @param the type of the reference + * @return {@code obj} if not {@code null} + * @throws NullPointerException if {@code obj} is {@code null} + */ + public static T requireNotNull(T obj) { + return Objects.requireNonNull(obj); + } + + /** + * 断言,必须不能为 null + *
+	 * public Foo(Bar bar, Baz baz) {
+	 *     this.bar = $.requireNotNull(bar, "bar must not be null");
+	 *     this.baz = $.requireNotNull(baz, "baz must not be null");
+	 * }
+	 * 
+ * + * @param obj the object reference to check for nullity + * @param message detail message to be used in the event that a {@code + * NullPointerException} is thrown + * @param the type of the reference + * @return {@code obj} if not {@code null} + * @throws NullPointerException if {@code obj} is {@code null} + */ + public static T requireNotNull(T obj, String message) { + return Objects.requireNonNull(obj, message); + } + + /** + * 断言,必须不能为 null + *
+	 * public Foo(Bar bar, Baz baz) {
+	 *     this.bar = $.requireNotNull(bar, () -> "bar must not be null");
+	 * }
+	 * 
+ * + * @param obj the object reference to check for nullity + * @param messageSupplier supplier of the detail message to be + * used in the event that a {@code NullPointerException} is thrown + * @param the type of the reference + * @return {@code obj} if not {@code null} + * @throws NullPointerException if {@code obj} is {@code null} + */ + public static T requireNotNull(T obj, Supplier messageSupplier) { + return Objects.requireNonNull(obj, messageSupplier); + } + + /** + * 判断对象是否为null + *

+ * This method exists to be used as a + * {@link java.util.function.Predicate}, {@code filter($::isNull)} + *

+ * + * @param obj a reference to be checked against {@code null} + * @return {@code true} if the provided reference is {@code null} otherwise + * {@code false} + * @see java.util.function.Predicate + */ + public static boolean isNull(@Nullable Object obj) { + return Objects.isNull(obj); + } + + /** + * 判断对象是否 not null + *

+ * This method exists to be used as a + * {@link java.util.function.Predicate}, {@code filter($::notNull)} + *

+ * + * @param obj a reference to be checked against {@code null} + * @return {@code true} if the provided reference is non-{@code null} + * otherwise {@code false} + * @see java.util.function.Predicate + */ + public static boolean notNull(@Nullable Object obj) { + return Objects.nonNull(obj); + } + + /** + * 首字母变小写 + * + * @param str 字符串 + * @return {String} + */ + public static String firstCharToLower(String str) { + return StringUtil.firstCharToLower(str); + } + + /** + * 首字母变大写 + * + * @param str 字符串 + * @return {String} + */ + public static String firstCharToUpper(String str) { + return StringUtil.firstCharToUpper(str); + } + + /** + * 判断是否为空字符串 + *
+	 * $.isBlank(null)		= true
+	 * $.isBlank("")		= true
+	 * $.isBlank(" ")		= true
+	 * $.isBlank("12345")	= false
+	 * $.isBlank(" 12345 ")	= false
+	 * 
+ * + * @param cs the {@code CharSequence} to check (may be {@code null}) + * @return {@code true} if the {@code CharSequence} is not {@code null}, + * its length is greater than 0, and it does not contain whitespace only + * @see Character#isWhitespace + */ + public static boolean isBlank(@Nullable final CharSequence cs) { + return StringUtil.isBlank(cs); + } + + /** + * 判断不为空字符串 + *
+	 * $.isNotBlank(null)	= false
+	 * $.isNotBlank("")		= false
+	 * $.isNotBlank(" ")	= false
+	 * $.isNotBlank("bob")	= true
+	 * $.isNotBlank("  bob  ") = true
+	 * 
+ * + * @param cs the CharSequence to check, may be null + * @return {@code true} if the CharSequence is + * not empty and not null and not whitespace + * @see Character#isWhitespace + */ + public static boolean isNotBlank(@Nullable final CharSequence cs) { + return StringUtil.isNotBlank(cs); + } + + /** + * 判断是否有任意一个 空字符串 + * + * @param css CharSequence + * @return boolean + */ + public static boolean isAnyBlank(final CharSequence... css) { + return StringUtil.isAnyBlank(css); + } + + /** + * 判断是否全为非空字符串 + * + * @param css CharSequence + * @return boolean + */ + public static boolean isNoneBlank(final CharSequence... css) { + return StringUtil.isNoneBlank(css); + } + + /** + * 判断对象是数组 + * + * @param obj the object to check + * @return 是否数组 + */ + public static boolean isArray(@Nullable Object obj) { + return ObjectUtil.isArray(obj); + } + + /** + * 判断空对象 object、map、list、set、字符串、数组 + * + * @param obj the object to check + * @return 数组是否为空 + */ + public static boolean isEmpty(@Nullable Object obj) { + return ObjectUtil.isEmpty(obj); + } + + /** + * 对象不为空 object、map、list、set、字符串、数组 + * + * @param obj the object to check + * @return 是否不为空 + */ + public static boolean isNotEmpty(@Nullable Object obj) { + return !ObjectUtil.isEmpty(obj); + } + + /** + * 判断数组为空 + * + * @param array the array to check + * @return 数组是否为空 + */ + public static boolean isEmpty(@Nullable Object[] array) { + return ObjectUtil.isEmpty(array); + } + + /** + * 判断数组不为空 + * + * @param array 数组 + * @return 数组是否不为空 + */ + public static boolean isNotEmpty(@Nullable Object[] array) { + return ObjectUtil.isNotEmpty(array); + } + + /** + * 对象组中是否存在 Empty Object + * + * @param os 对象组 + * @return boolean + */ + public static boolean hasEmpty(Object... os) { + for (Object o : os) { + if (isEmpty(o)) { + return true; + } + } + return false; + } + + /** + * 对象组中是否全部为 Empty Object + * + * @param os 对象组 + * @return boolean + */ + public static boolean isAllEmpty(Object... os) { + for (Object o : os) { + if (isNotEmpty(o)) { + return false; + } + } + return true; + } + + /** + * 将字符串中特定模式的字符转换成map中对应的值 + *

+ * use: format("my name is ${name}, and i like ${like}!", {"name":"L.cm", "like": "Java"}) + * + * @param message 需要转换的字符串 + * @param params 转换所需的键值对集合 + * @return 转换后的字符串 + */ + public static String format(@Nullable String message, @Nullable Map params) { + return StringUtil.format(message, params); + } + + /** + * 同 log 格式的 format 规则 + *

+ * use: format("my name is {}, and i like {}!", "L.cm", "Java") + * + * @param message 需要转换的字符串 + * @param arguments 需要替换的变量 + * @return 转换后的字符串 + */ + public static String format(@Nullable String message, @Nullable Object... arguments) { + return StringUtil.format(message, arguments); + } + + /** + * 格式化执行时间,单位为 ms 和 s,保留三位小数 + * + * @param nanos 纳秒 + * @return 格式化后的时间 + */ + public static String format(long nanos) { + return StringUtil.format(nanos); + } + + /** + * 比较两个对象是否相等。
+ * 相同的条件有两个,满足其一即可:
+ * + * @param obj1 对象1 + * @param obj2 对象2 + * @return 是否相等 + */ + public static boolean equals(Object obj1, Object obj2) { + return Objects.equals(obj1, obj2); + } + + /** + * 安全的 equals + * + * @param o1 first Object to compare + * @param o2 second Object to compare + * @return whether the given objects are equal + * @see Object#equals(Object) + * @see java.util.Arrays#equals + */ + public static boolean equalsSafe(@Nullable Object o1, @Nullable Object o2) { + return ObjectUtil.nullSafeEquals(o1, o2); + } + + /** + * 判断数组中是否包含元素 + * + * @param array the Array to check + * @param element the element to look for + * @param The generic tag + * @return {@code true} if found, {@code false} else + */ + public static boolean contains(@Nullable T[] array, final T element) { + return CollectionUtil.contains(array, element); + } + + /** + * 判断迭代器中是否包含元素 + * + * @param iterator the Iterator to check + * @param element the element to look for + * @return {@code true} if found, {@code false} otherwise + */ + public static boolean contains(@Nullable Iterator iterator, Object element) { + return CollectionUtil.contains(iterator, element); + } + + /** + * 判断枚举是否包含该元素 + * + * @param enumeration the Enumeration to check + * @param element the element to look for + * @return {@code true} if found, {@code false} otherwise + */ + public static boolean contains(@Nullable Enumeration enumeration, Object element) { + return CollectionUtil.contains(enumeration, element); + } + + /** + * 不可变 Set + * + * @param es 对象 + * @param 泛型 + * @return 集合 + */ + @SafeVarargs + public static Set ofImmutableSet(E... es) { + return CollectionUtil.ofImmutableSet(es); + } + + /** + * 不可变 List + * + * @param es 对象 + * @param 泛型 + * @return 集合 + */ + @SafeVarargs + public static List ofImmutableList(E... es) { + return CollectionUtil.ofImmutableList(es); + } + + /** + * 强转string,并去掉多余空格 + * + * @param str 字符串 + * @return {String} + */ + public static String toStr(Object str) { + return toStr(str, ""); + } + + /** + * 强转string,并去掉多余空格 + * + * @param str 字符串 + * @param defaultValue 默认值 + * @return {String} + */ + public static String toStr(Object str, String defaultValue) { + if (null == str || str.equals(StringPool.NULL)) { + return defaultValue; + } + return String.valueOf(str); + } + + /** + * 强转string(包含空字符串),并去掉多余空格 + * + * @param str 字符串 + * @param defaultValue 默认值 + * @return {String} + */ + public static String toStrWithEmpty(Object str, String defaultValue) { + if (null == str || str.equals(StringPool.NULL) || str.equals(StringPool.EMPTY)) { + return defaultValue; + } + return String.valueOf(str); + } + + + /** + * 判断一个字符串是否是数字 + * + * @param cs the CharSequence to check, may be null + * @return {boolean} + */ + public static boolean isNumeric(final CharSequence cs) { + return StringUtil.isNumeric(cs); + } + + /** + * 字符串转 int,为空则返回0 + * + *

+	 *   $.toInt(null) = 0
+	 *   $.toInt("")   = 0
+	 *   $.toInt("1")  = 1
+	 * 
+ * + * @param str the string to convert, may be null + * @return the int represented by the string, or zero if + * conversion fails + */ + public static int toInt(final Object str) { + return NumberUtil.toInt(String.valueOf(str)); + } + + /** + * 字符串转 int,为空则返回默认值 + * + *
+	 *   $.toInt(null, 1) = 1
+	 *   $.toInt("", 1)   = 1
+	 *   $.toInt("1", 0)  = 1
+	 * 
+ * + * @param str the string to convert, may be null + * @param defaultValue the default value + * @return the int represented by the string, or the default if conversion fails + */ + public static int toInt(@Nullable final Object str, final int defaultValue) { + return NumberUtil.toInt(String.valueOf(str), defaultValue); + } + + /** + * 字符串转 long,为空则返回0 + * + *
+	 *   $.toLong(null) = 0L
+	 *   $.toLong("")   = 0L
+	 *   $.toLong("1")  = 1L
+	 * 
+ * + * @param str the string to convert, may be null + * @return the long represented by the string, or 0 if + * conversion fails + */ + public static long toLong(final Object str) { + return NumberUtil.toLong(String.valueOf(str)); + } + + /** + * 字符串转 long,为空则返回默认值 + * + *
+	 *   $.toLong(null, 1L) = 1L
+	 *   $.toLong("", 1L)   = 1L
+	 *   $.toLong("1", 0L)  = 1L
+	 * 
+ * + * @param str the string to convert, may be null + * @param defaultValue the default value + * @return the long represented by the string, or the default if conversion fails + */ + public static long toLong(@Nullable final Object str, final long defaultValue) { + return NumberUtil.toLong(String.valueOf(str), defaultValue); + } + + /** + *

Convert a String to an Double, returning a + * default value if the conversion fails.

+ * + *

If the string is null, the default value is returned.

+ * + *
+	 *   $.toDouble(null, 1) = 1.0
+	 *   $.toDouble("", 1)   = 1.0
+	 *   $.toDouble("1", 0)  = 1.0
+	 * 
+ * + * @param value the string to convert, may be null + * @return the int represented by the string, or the default if conversion fails + */ + public static Double toDouble(Object value) { + return toDouble(String.valueOf(value), -1.00); + } + + /** + *

Convert a String to an Double, returning a + * default value if the conversion fails.

+ * + *

If the string is null, the default value is returned.

+ * + *
+	 *   $.toDouble(null, 1) = 1.0
+	 *   $.toDouble("", 1)   = 1.0
+	 *   $.toDouble("1", 0)  = 1.0
+	 * 
+ * + * @param value the string to convert, may be null + * @param defaultValue the default value + * @return the int represented by the string, or the default if conversion fails + */ + public static Double toDouble(Object value, Double defaultValue) { + return NumberUtil.toDouble(String.valueOf(value), defaultValue); + } + + /** + *

Convert a String to an Float, returning a + * default value if the conversion fails.

+ * + *

If the string is null, the default value is returned.

+ * + *
+	 *   $.toFloat(null, 1) = 1.00f
+	 *   $.toFloat("", 1)   = 1.00f
+	 *   $.toFloat("1", 0)  = 1.00f
+	 * 
+ * + * @param value the string to convert, may be null + * @return the int represented by the string, or the default if conversion fails + */ + public static Float toFloat(Object value) { + return toFloat(String.valueOf(value), -1.0f); + } + + /** + *

Convert a String to an Float, returning a + * default value if the conversion fails.

+ * + *

If the string is null, the default value is returned.

+ * + *
+	 *   $.toFloat(null, 1) = 1.00f
+	 *   $.toFloat("", 1)   = 1.00f
+	 *   $.toFloat("1", 0)  = 1.00f
+	 * 
+ * + * @param value the string to convert, may be null + * @param defaultValue the default value + * @return the int represented by the string, or the default if conversion fails + */ + public static Float toFloat(Object value, Float defaultValue) { + return NumberUtil.toFloat(String.valueOf(value), defaultValue); + } + + /** + *

Convert a String to an Boolean, returning a + * default value if the conversion fails.

+ * + *

If the string is null, the default value is returned.

+ * + *
+	 *   $.toBoolean("true", true)  = true
+	 *   $.toBoolean("false")   	= false
+	 *   $.toBoolean("", false)  	= false
+	 * 
+ * + * @param value the string to convert, may be null + * @return the int represented by the string, or the default if conversion fails + */ + public static Boolean toBoolean(Object value) { + return toBoolean(value, null); + } + + /** + *

Convert a String to an Boolean, returning a + * default value if the conversion fails.

+ * + *

If the string is null, the default value is returned.

+ * + *
+	 *   $.toBoolean("true", true)  = true
+	 *   $.toBoolean("false")   	= false
+	 *   $.toBoolean("", false)  	= false
+	 * 
+ * + * @param value the string to convert, may be null + * @param defaultValue the default value + * @return the int represented by the string, or the default if conversion fails + */ + public static Boolean toBoolean(Object value, Boolean defaultValue) { + if (value != null) { + String val = String.valueOf(value); + val = val.toLowerCase().trim(); + return Boolean.parseBoolean(val); + } + return defaultValue; + } + + /** + * 转换为Integer数组
+ * + * @param str 被转换的值 + * @return 结果 + */ + public static Integer[] toIntArray(String str) { + return toIntArray(",", str); + } + + /** + * 转换为Integer数组
+ * + * @param split 分隔符 + * @param str 被转换的值 + * @return 结果 + */ + public static Integer[] toIntArray(String split, String str) { + if (StringUtil.isEmpty(str)) { + return new Integer[]{}; + } + String[] arr = str.split(split); + final Integer[] ints = new Integer[arr.length]; + for (int i = 0; i < arr.length; i++) { + final Integer v = toInt(arr[i], 0); + ints[i] = v; + } + return ints; + } + + /** + * 转换为Integer集合
+ * + * @param str 结果被转换的值 + * @return 结果 + */ + public static List toIntList(String str) { + return Arrays.asList(toIntArray(str)); + } + + /** + * 转换为Integer集合
+ * + * @param split 分隔符 + * @param str 被转换的值 + * @return 结果 + */ + public static List toIntList(String split, String str) { + return Arrays.asList(toIntArray(split, str)); + } + + /** + * 获取第一位Integer数值 + * + * @param str 被转换的值 + * @return 结果 + */ + public static Integer firstInt(String str) { + return firstInt(",", str); + } + + /** + * 获取第一位Integer数值 + * + * @param split 分隔符 + * @param str 被转换的值 + * @return 结果 + */ + public static Integer firstInt(String split, String str) { + List ints = toIntList(split, str); + if (isEmpty(ints)) { + return null; + } else { + return ints.get(0); + } + } + + /** + * 转换为Long数组
+ * + * @param str 被转换的值 + * @return 结果 + */ + public static Long[] toLongArray(String str) { + return toLongArray(",", str); + } + + /** + * 转换为Long数组
+ * + * @param split 分隔符 + * @param str 被转换的值 + * @return 结果 + */ + public static Long[] toLongArray(String split, String str) { + if (StringUtil.isEmpty(str)) { + return new Long[]{}; + } + String[] arr = str.split(split); + final Long[] longs = new Long[arr.length]; + for (int i = 0; i < arr.length; i++) { + final Long v = toLong(arr[i], 0); + longs[i] = v; + } + return longs; + } + + /** + * 转换为Long集合
+ * + * @param str 结果被转换的值 + * @return 结果 + */ + public static List toLongList(String str) { + return Arrays.asList(toLongArray(str)); + } + + /** + * 转换为Long集合
+ * + * @param split 分隔符 + * @param str 被转换的值 + * @return 结果 + */ + public static List toLongList(String split, String str) { + return Arrays.asList(toLongArray(split, str)); + } + + /** + * 获取第一位Long数值 + * + * @param str 被转换的值 + * @return 结果 + */ + public static Long firstLong(String str) { + return firstLong(",", str); + } + + /** + * 获取第一位Long数值 + * + * @param split 分隔符 + * @param str 被转换的值 + * @return 结果 + */ + public static Long firstLong(String split, String str) { + List longs = toLongList(split, str); + if (isEmpty(longs)) { + return null; + } else { + return longs.get(0); + } + } + + /** + * 转换为String数组
+ * + * @param str 被转换的值 + * @return 结果 + */ + public static String[] toStrArray(String str) { + return toStrArray(",", str); + } + + /** + * 转换为String数组
+ * + * @param split 分隔符 + * @param str 被转换的值 + * @return 结果 + */ + public static String[] toStrArray(String split, String str) { + if (isBlank(str)) { + return new String[]{}; + } + return str.split(split); + } + + /** + * 转换为String集合
+ * + * @param str 结果被转换的值 + * @return 结果 + */ + public static List toStrList(String str) { + return Arrays.asList(toStrArray(str)); + } + + /** + * 转换为String集合
+ * + * @param split 分隔符 + * @param str 被转换的值 + * @return 结果 + */ + public static List toStrList(String split, String str) { + return Arrays.asList(toStrArray(split, str)); + } + + /** + * 获取第一位String数值 + * + * @param str 被转换的值 + * @return 结果 + */ + public static String firstStr(String str) { + return firstStr(",", str); + } + + /** + * 获取第一位String数值 + * + * @param split 分隔符 + * @param str 被转换的值 + * @return 结果 + */ + public static String firstStr(String split, String str) { + List strs = toStrList(split, str); + if (isEmpty(strs)) { + return null; + } else { + return strs.get(0); + } + } + + /** + * 将 long 转短字符串 为 62 进制 + * + * @param num 数字 + * @return 短字符串 + */ + public static String to62String(long num) { + return NumberUtil.to62String(num); + } + + /** + * 将集合拼接成字符串,默认使用`,`拼接 + * + * @param coll the {@code Collection} to convert + * @return the delimited {@code String} + */ + public static String join(Collection coll) { + return StringUtil.join(coll); + } + + /** + * 将集合拼接成字符串,默认指定分隔符 + * + * @param coll the {@code Collection} to convert + * @param delim the delimiter to use (typically a ",") + * @return the delimited {@code String} + */ + public static String join(Collection coll, String delim) { + return StringUtil.join(coll, delim); + } + + /** + * 将数组拼接成字符串,默认使用`,`拼接 + * + * @param arr the array to display + * @return the delimited {@code String} + */ + public static String join(Object[] arr) { + return StringUtil.join(arr); + } + + /** + * 将数组拼接成字符串,默认指定分隔符 + * + * @param arr the array to display + * @param delim the delimiter to use (typically a ",") + * @return the delimited {@code String} + */ + public static String join(Object[] arr, String delim) { + return StringUtil.join(arr, delim); + } + + /** + * 切分字符串,不去除切分后每个元素两边的空白符,不去除空白项 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @return 切分后的集合 + */ + public static List split(CharSequence str, char separator) { + return StringUtil.split(str, separator, -1); + } + + /** + * 切分字符串,去除切分后每个元素两边的空白符,去除空白项 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @return 切分后的集合 + */ + public static List splitTrim(CharSequence str, char separator) { + return StringUtil.splitTrim(str, separator); + } + + /** + * 切分字符串,去除切分后每个元素两边的空白符,去除空白项 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @return 切分后的集合 + */ + public static List splitTrim(CharSequence str, CharSequence separator) { + return StringUtil.splitTrim(str, separator); + } + + /** + * 分割 字符串 + * + * @param str 字符串 + * @param delimiter 分割符 + * @return 字符串数组 + */ + public static String[] split(@Nullable String str, @Nullable String delimiter) { + return StringUtil.delimitedListToStringArray(str, delimiter); + } + + /** + * 分割 字符串 删除常见 空白符 + * + * @param str 字符串 + * @param delimiter 分割符 + * @return 字符串数组 + */ + public static String[] splitTrim(@Nullable String str, @Nullable String delimiter) { + return StringUtil.delimitedListToStringArray(str, delimiter, " \t\n\n\f"); + } + + /** + * 字符串是否符合指定的 表达式 + * + *

+ * pattern styles: "xxx*", "*xxx", "*xxx*" and "xxx*yyy" + *

+ * + * @param pattern 表达式 + * @param str 字符串 + * @return 是否匹配 + */ + public static boolean simpleMatch(@Nullable String pattern, @Nullable String str) { + return PatternMatchUtils.simpleMatch(pattern, str); + } + + /** + * 字符串是否符合指定的 表达式 + * + *

+ * pattern styles: "xxx*", "*xxx", "*xxx*" and "xxx*yyy" + *

+ * + * @param patterns 表达式 数组 + * @param str 字符串 + * @return 是否匹配 + */ + public static boolean simpleMatch(@Nullable String[] patterns, String str) { + return PatternMatchUtils.simpleMatch(patterns, str); + } + + /** + * 生成uuid + * + * @return UUID + */ + public static String randomUUID() { + return StringUtil.randomUUID(); + } + + /** + * 转义HTML用于安全过滤 + * + * @param html html + * @return {String} + */ + public static String escapeHtml(String html) { + return StringUtil.escapeHtml(html); + } + + /** + * 随机数生成 + * + * @param count 字符长度 + * @return 随机数 + */ + public static String random(int count) { + return StringUtil.random(count); + } + + /** + * 随机数生成 + * + * @param count 字符长度 + * @param randomType 随机数类别 + * @return 随机数 + */ + public static String random(int count, RandomType randomType) { + return StringUtil.random(count, randomType); + } + + /** + * 字符串序列化成 md5 + * + * @param data Data to digest + * @return MD5 digest as a hex string + */ + public static String md5Hex(final String data) { + return DigestUtil.md5Hex(data); + } + + /** + * 数组序列化成 md5 + * + * @param bytes the bytes to calculate the digest over + * @return md5 digest string + */ + public static String md5Hex(final byte[] bytes) { + return DigestUtil.md5Hex(bytes); + } + + + /** + * sha1Hex + * + * @param data Data to digest + * @return digest as a hex string + */ + public static String sha1Hex(String data) { + return DigestUtil.sha1Hex(data); + } + + /** + * sha1Hex + * + * @param bytes Data to digest + * @return digest as a hex string + */ + public static String sha1Hex(final byte[] bytes) { + return DigestUtil.sha1Hex(bytes); + } + + /** + * SHA224Hex + * + * @param data Data to digest + * @return digest as a hex string + */ + public static String sha224Hex(String data) { + return DigestUtil.sha224Hex(data); + } + + /** + * SHA224Hex + * + * @param bytes Data to digest + * @return digest as a hex string + */ + public static String sha224Hex(final byte[] bytes) { + return DigestUtil.sha224Hex(bytes); + } + + /** + * sha256Hex + * + * @param data Data to digest + * @return digest as a hex string + */ + public static String sha256Hex(String data) { + return DigestUtil.sha256Hex(data); + } + + /** + * sha256Hex + * + * @param bytes Data to digest + * @return digest as a hex string + */ + public static String sha256Hex(final byte[] bytes) { + return DigestUtil.sha256Hex(bytes); + } + + /** + * sha384Hex + * + * @param data Data to digest + * @return digest as a hex string + */ + public static String sha384Hex(String data) { + return DigestUtil.sha384Hex(data); + } + + /** + * sha384Hex + * + * @param bytes Data to digest + * @return digest as a hex string + */ + public static String sha384Hex(final byte[] bytes) { + return DigestUtil.sha384Hex(bytes); + } + + /** + * sha512Hex + * + * @param data Data to digest + * @return digest as a hex string + */ + public static String sha512Hex(String data) { + return DigestUtil.sha512Hex(data); + } + + /** + * sha512Hex + * + * @param bytes Data to digest + * @return digest as a hex string + */ + public static String sha512Hex(final byte[] bytes) { + return DigestUtil.sha512Hex(bytes); + } + + /** + * hmacMd5 Hex + * + * @param data Data to digest + * @param key key + * @return digest as a hex string + */ + public static String hmacMd5Hex(String data, String key) { + return DigestUtil.hmacMd5Hex(data, key); + } + + /** + * hmacMd5 Hex + * + * @param bytes Data to digest + * @param key key + * @return digest as a hex string + */ + public static String hmacMd5Hex(final byte[] bytes, String key) { + return DigestUtil.hmacMd5Hex(bytes, key); + } + + /** + * hmacSha1 Hex + * + * @param data Data to digest + * @param key key + * @return digest as a hex string + */ + public static String hmacSha1Hex(String data, String key) { + return DigestUtil.hmacSha1Hex(data, key); + } + + /** + * hmacSha1 Hex + * + * @param bytes Data to digest + * @param key key + * @return digest as a hex string + */ + public static String hmacSha1Hex(final byte[] bytes, String key) { + return DigestUtil.hmacSha1Hex(bytes, key); + } + + /** + * hmacSha224 Hex + * + * @param data Data to digest + * @param key key + * @return digest as a hex string + */ + public static String hmacSha224Hex(String data, String key) { + return DigestUtil.hmacSha224Hex(data, key); + } + + /** + * hmacSha224 Hex + * + * @param bytes Data to digest + * @param key key + * @return digest as a hex string + */ + public static String hmacSha224Hex(final byte[] bytes, String key) { + return DigestUtil.hmacSha224Hex(bytes, key); + } + + /** + * hmacSha256 Hex + * + * @param data Data to digest + * @param key key + * @return digest as a hex string + */ + public static String hmacSha256Hex(String data, String key) { + return DigestUtil.hmacSha256Hex(data, key); + } + + /** + * hmacSha256 Hex + * + * @param bytes Data to digest + * @param key key + * @return digest as a hex string + */ + public static String hmacSha256Hex(final byte[] bytes, String key) { + return DigestUtil.hmacSha256Hex(bytes, key); + } + + /** + * hmacSha384 Hex + * + * @param data Data to digest + * @param key key + * @return digest as a hex string + */ + public static String hmacSha384Hex(String data, String key) { + return DigestUtil.hmacSha384Hex(data, key); + } + + /** + * hmacSha384 Hex + * + * @param bytes Data to digest + * @param key key + * @return digest as a hex string + */ + public static String hmacSha384Hex(final byte[] bytes, String key) { + return DigestUtil.hmacSha384Hex(bytes, key); + } + + /** + * hmacSha512 Hex + * + * @param data Data to digest + * @param key key + * @return digest as a hex string + */ + public static String hmacSha512Hex(String data, String key) { + return DigestUtil.hmacSha512Hex(data, key); + } + + /** + * hmacSha512 Hex + * + * @param bytes Data to digest + * @param key key + * @return digest as a hex string + */ + public static String hmacSha512Hex(final byte[] bytes, String key) { + return DigestUtil.hmacSha512Hex(bytes, key); + } + + /** + * byte 数组序列化成 hex + * + * @param bytes bytes to encode + * @return MD5 digest as a hex string + */ + public static String encodeHex(byte[] bytes) { + return DigestUtil.encodeHex(bytes); + } + + /** + * 字符串反序列化成 hex + * + * @param hexString String to decode + * @return MD5 digest as a hex string + */ + public static byte[] decodeHex(final String hexString) { + return DigestUtil.decodeHex(hexString); + } + + /** + * Base64编码 + * + * @param value 字符串 + * @return {String} + */ + public static String encodeBase64(String value) { + return Base64Util.encode(value); + } + + /** + * Base64编码 + * + * @param value 字符串 + * @param charset 字符集 + * @return {String} + */ + public static String encodeBase64(String value, Charset charset) { + return Base64Util.encode(value, charset); + } + + /** + * Base64编码为URL安全 + * + * @param value 字符串 + * @return {String} + */ + public static String encodeBase64UrlSafe(String value) { + return Base64Util.encodeUrlSafe(value); + } + + /** + * Base64编码为URL安全 + * + * @param value 字符串 + * @param charset 字符集 + * @return {String} + */ + public static String encodeBase64UrlSafe(String value, Charset charset) { + return Base64Util.encodeUrlSafe(value, charset); + } + + /** + * Base64解码 + * + * @param value 字符串 + * @return {String} + */ + public static String decodeBase64(String value) { + return Base64Util.decode(value); + } + + /** + * Base64解码 + * + * @param value 字符串 + * @param charset 字符集 + * @return {String} + */ + public static String decodeBase64(String value, Charset charset) { + return Base64Util.decode(value, charset); + } + + /** + * Base64URL安全解码 + * + * @param value 字符串 + * @return {String} + */ + public static String decodeBase64UrlSafe(String value) { + return Base64Util.decodeUrlSafe(value); + } + + /** + * Base64URL安全解码 + * + * @param value 字符串 + * @param charset 字符集 + * @return {String} + */ + public static String decodeBase64UrlSafe(String value, Charset charset) { + return Base64Util.decodeUrlSafe(value, charset); + } + + /** + * 关闭 Closeable + * + * @param closeable 自动关闭 + */ + public static void closeQuietly(@Nullable Closeable closeable) { + IoUtil.closeQuietly(closeable); + } + + /** + * InputStream to String utf-8 + * + * @param input the InputStream to read from + * @return the requested String + * @throws NullPointerException if the input is null + */ + public static String readToString(InputStream input) { + return IoUtil.readToString(input); + } + + /** + * InputStream to String + * + * @param input the InputStream to read from + * @param charset the Charset + * @return the requested String + * @throws NullPointerException if the input is null + */ + public static String readToString(@Nullable InputStream input, Charset charset) { + return IoUtil.readToString(input, charset); + } + + /** + * InputStream to bytes 数组 + * + * @param input InputStream + * @return the requested byte array + */ + public static byte[] readToByteArray(@Nullable InputStream input) { + return IoUtil.readToByteArray(input); + } + + /** + * 读取文件为字符串 + * + * @param file the file to read, must not be {@code null} + * @return the file contents, never {@code null} + */ + public static String readToString(final File file) { + return FileUtil.readToString(file); + } + + /** + * 读取文件为字符串 + * + * @param file the file to read, must not be {@code null} + * @param encoding the encoding to use, {@code null} means platform default + * @return the file contents, never {@code null} + */ + public static String readToString(File file, Charset encoding) { + return FileUtil.readToString(file, encoding); + } + + /** + * 读取文件为 byte 数组 + * + * @param file the file to read, must not be {@code null} + * @return the file contents, never {@code null} + */ + public static byte[] readToByteArray(File file) { + return FileUtil.readToByteArray(file); + } + + /** + * 将对象序列化成json字符串 + * + * @param object javaBean + * @return jsonString json字符串 + */ + public static String toJson(Object object) { + return JsonUtil.toJson(object); + } + + /** + * 将对象序列化成 json byte 数组 + * + * @param object javaBean + * @return jsonString json字符串 + */ + public static byte[] toJsonAsBytes(Object object) { + return JsonUtil.toJsonAsBytes(object); + } + + /** + * 将json字符串转成 JsonNode + * + * @param jsonString jsonString + * @return jsonString json字符串 + */ + public static JsonNode readTree(String jsonString) { + return JsonUtil.readTree(jsonString); + } + + /** + * 将json字符串转成 JsonNode + * + * @param in InputStream + * @return jsonString json字符串 + */ + public static JsonNode readTree(InputStream in) { + return JsonUtil.readTree(in); + } + + /** + * 将json字符串转成 JsonNode + * + * @param content content + * @return jsonString json字符串 + */ + public static JsonNode readTree(byte[] content) { + return JsonUtil.readTree(content); + } + + /** + * 将json字符串转成 JsonNode + * + * @param jsonParser JsonParser + * @return jsonString json字符串 + */ + public static JsonNode readTree(JsonParser jsonParser) { + return JsonUtil.readTree(jsonParser); + } + + /** + * 将json byte 数组反序列化成对象 + * + * @param bytes json bytes + * @param valueType class + * @param T 泛型标记 + * @return Bean + */ + public static T readJson(byte[] bytes, Class valueType) { + return JsonUtil.parse(bytes, valueType); + } + + /** + * 将json反序列化成对象 + * + * @param jsonString jsonString + * @param valueType class + * @param T 泛型标记 + * @return Bean + */ + public static T readJson(String jsonString, Class valueType) { + return JsonUtil.parse(jsonString, valueType); + } + + /** + * 将json反序列化成对象 + * + * @param in InputStream + * @param valueType class + * @param T 泛型标记 + * @return Bean + */ + public static T readJson(InputStream in, Class valueType) { + return JsonUtil.parse(in, valueType); + } + + /** + * 将json反序列化成对象 + * + * @param bytes bytes + * @param typeReference 泛型类型 + * @param T 泛型标记 + * @return Bean + */ + public static T readJson(byte[] bytes, TypeReference typeReference) { + return JsonUtil.parse(bytes, typeReference); + } + + /** + * 将json反序列化成对象 + * + * @param jsonString jsonString + * @param typeReference 泛型类型 + * @param T 泛型标记 + * @return Bean + */ + public static T readJson(String jsonString, TypeReference typeReference) { + return JsonUtil.parse(jsonString, typeReference); + } + + /** + * 将json反序列化成对象 + * + * @param in InputStream + * @param typeReference 泛型类型 + * @param T 泛型标记 + * @return Bean + */ + public static T readJson(InputStream in, TypeReference typeReference) { + return JsonUtil.parse(in, typeReference); + } + + /** + * url 编码 + * + * @param source the String to be encoded + * @return the encoded String + */ + public static String urlEncode(String source) { + return UrlUtil.encode(source, Charsets.UTF_8); + } + + /** + * url 编码 + * + * @param source the String to be encoded + * @param charset the character encoding to encode to + * @return the encoded String + */ + public static String urlEncode(String source, Charset charset) { + return UrlUtil.encode(source, charset); + } + + /** + * url 解码 + * + * @param source the encoded String + * @return the decoded value + * @throws IllegalArgumentException when the given source contains invalid encoded sequences + * @see StringUtils#uriDecode(String, Charset) + * @see java.net.URLDecoder#decode(String, String) + */ + public static String urlDecode(String source) { + return StringUtils.uriDecode(source, Charsets.UTF_8); + } + + /** + * url 解码 + * + * @param source the encoded String + * @param charset the character encoding to use + * @return the decoded value + * @throws IllegalArgumentException when the given source contains invalid encoded sequences + * @see StringUtils#uriDecode(String, Charset) + * @see java.net.URLDecoder#decode(String, String) + */ + public static String urlDecode(String source, Charset charset) { + return StringUtils.uriDecode(source, charset); + } + + /** + * 日期时间格式化 + * + * @param date 时间 + * @return 格式化后的时间 + */ + public static String formatDateTime(Date date) { + return DateUtil.formatDateTime(date); + } + + /** + * 日期格式化 + * + * @param date 时间 + * @return 格式化后的时间 + */ + public static String formatDate(Date date) { + return DateUtil.formatDate(date); + } + + /** + * 时间格式化 + * + * @param date 时间 + * @return 格式化后的时间 + */ + public static String formatTime(Date date) { + return DateUtil.formatTime(date); + } + + /** + * 对象格式化 支持数字,date,java8时间 + * + * @param object 格式化对象 + * @param pattern 表达式 + * @return 格式化后的字符串 + */ + public static String format(Object object, String pattern) { + if (object instanceof Number) { + DecimalFormat decimalFormat = new DecimalFormat(pattern); + return decimalFormat.format(object); + } else if (object instanceof Date) { + return DateUtil.format((Date) object, pattern); + } else if (object instanceof TemporalAccessor) { + return DateTimeUtil.format((TemporalAccessor) object, pattern); + } + throw new IllegalArgumentException("未支持的对象:" + object + ",格式:" + object); + } + + /** + * 将字符串转换为时间 + * + * @param dateStr 时间字符串 + * @param pattern 表达式 + * @return 时间 + */ + public static Date parseDate(String dateStr, String pattern) { + return DateUtil.parse(dateStr, pattern); + } + + /** + * 将字符串转换为时间 + * + * @param dateStr 时间字符串 + * @param format ConcurrentDateFormat + * @return 时间 + */ + public static Date parse(String dateStr, ConcurrentDateFormat format) { + return DateUtil.parse(dateStr, format); + } + + /** + * 日期时间格式化 + * + * @param temporal 时间 + * @return 格式化后的时间 + */ + public static String formatDateTime(TemporalAccessor temporal) { + return DateTimeUtil.formatDateTime(temporal); + } + + /** + * 日期时间格式化 + * + * @param temporal 时间 + * @return 格式化后的时间 + */ + public static String formatDate(TemporalAccessor temporal) { + return DateTimeUtil.formatDate(temporal); + } + + /** + * 时间格式化 + * + * @param temporal 时间 + * @return 格式化后的时间 + */ + public static String formatTime(TemporalAccessor temporal) { + return DateTimeUtil.formatTime(temporal); + } + + /** + * 将字符串转换为时间 + * + * @param dateStr 时间字符串 + * @param formatter DateTimeFormatter + * @return 时间 + */ + public static LocalDateTime parseDateTime(String dateStr, DateTimeFormatter formatter) { + return DateTimeUtil.parseDateTime(dateStr, formatter); + } + + /** + * 将字符串转换为时间 + * + * @param dateStr 时间字符串 + * @return 时间 + */ + public static LocalDateTime parseDateTime(String dateStr) { + return DateTimeUtil.parseDateTime(dateStr); + } + + /** + * 将字符串转换为时间 + * + * @param dateStr 时间字符串 + * @param formatter DateTimeFormatter + * @return 时间 + */ + public static LocalDate parseDate(String dateStr, DateTimeFormatter formatter) { + return DateTimeUtil.parseDate(dateStr, formatter); + } + + /** + * 将字符串转换为日期 + * + * @param dateStr 时间字符串 + * @return 时间 + */ + public static LocalDate parseDate(String dateStr) { + return DateTimeUtil.parseDate(dateStr, DateTimeUtil.DATE_FORMAT); + } + + /** + * 将字符串转换为时间 + * + * @param dateStr 时间字符串 + * @param formatter DateTimeFormatter + * @return 时间 + */ + public static LocalTime parseTime(String dateStr, DateTimeFormatter formatter) { + return DateTimeUtil.parseTime(dateStr, formatter); + } + + /** + * 将字符串转换为时间 + * + * @param dateStr 时间字符串 + * @return 时间 + */ + public static LocalTime parseTime(String dateStr) { + return DateTimeUtil.parseTime(dateStr); + } + + /** + * 时间比较 + * + * @param startInclusive the start instant, inclusive, not null + * @param endExclusive the end instant, exclusive, not null + * @return a {@code Duration}, not null + */ + public static Duration between(Temporal startInclusive, Temporal endExclusive) { + return Duration.between(startInclusive, endExclusive); + } + + /** + * 比较2个 时间差 + * + * @param startDate 开始时间 + * @param endDate 结束时间 + * @return 时间间隔 + */ + public static Duration between(Date startDate, Date endDate) { + return DateUtil.between(startDate, endDate); + } + + /** + * 对象类型转换 + * + * @param source the source object + * @param targetType the target type + * @param 泛型标记 + * @return the converted value + * @throws IllegalArgumentException if targetType is {@code null}, + * or sourceType is {@code null} but source is not {@code null} + */ + @Nullable + public static T convert(@Nullable Object source, Class targetType) { + return ConvertUtil.convert(source, targetType); + } + + /** + * 对象类型转换 + * + * @param source the source object + * @param sourceType the source type + * @param targetType the target type + * @param 泛型标记 + * @return the converted value + * @throws IllegalArgumentException if targetType is {@code null}, + * or sourceType is {@code null} but source is not {@code null} + */ + @Nullable + public static T convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + return ConvertUtil.convert(source, sourceType, targetType); + } + + /** + * 对象类型转换 + * + * @param source the source object + * @param targetType the target type + * @param 泛型标记 + * @return the converted value + * @throws IllegalArgumentException if targetType is {@code null}, + * or sourceType is {@code null} but source is not {@code null} + */ + @Nullable + public static T convert(@Nullable Object source, TypeDescriptor targetType) { + return ConvertUtil.convert(source, targetType); + } + + /** + * 获取方法参数信息 + * + * @param constructor 构造器 + * @param parameterIndex 参数序号 + * @return {MethodParameter} + */ + public static MethodParameter getMethodParameter(Constructor constructor, int parameterIndex) { + return ClassUtil.getMethodParameter(constructor, parameterIndex); + } + + /** + * 获取方法参数信息 + * + * @param method 方法 + * @param parameterIndex 参数序号 + * @return {MethodParameter} + */ + public static MethodParameter getMethodParameter(Method method, int parameterIndex) { + return ClassUtil.getMethodParameter(method, parameterIndex); + } + + /** + * 获取Annotation注解 + * + * @param annotatedElement AnnotatedElement + * @param annotationType 注解类 + * @param
泛型标记 + * @return {Annotation} + */ + @Nullable + public static A getAnnotation(AnnotatedElement annotatedElement, Class annotationType) { + return AnnotatedElementUtils.findMergedAnnotation(annotatedElement, annotationType); + } + + /** + * 获取Annotation,先找方法,没有则再找方法上的类 + * + * @param method Method + * @param annotationType 注解类 + * @param 泛型标记 + * @return {Annotation} + */ + @Nullable + public static A getAnnotation(Method method, Class annotationType) { + return ClassUtil.getAnnotation(method, annotationType); + } + + /** + * 获取Annotation,先找HandlerMethod,没有则再找对应的类 + * + * @param handlerMethod HandlerMethod + * @param annotationType 注解类 + * @param 泛型标记 + * @return {Annotation} + */ + @Nullable + public static A getAnnotation(HandlerMethod handlerMethod, Class annotationType) { + return ClassUtil.getAnnotation(handlerMethod, annotationType); + } + + /** + * 实例化对象 + * + * @param clazz 类 + * @param 泛型标记 + * @return 对象 + */ + @SuppressWarnings("unchecked") + public static T newInstance(Class clazz) { + return (T) BeanUtil.instantiateClass(clazz); + } + + /** + * 实例化对象 + * + * @param clazzStr 类名 + * @param 泛型标记 + * @return 对象 + */ + public static T newInstance(String clazzStr) { + return BeanUtil.newInstance(clazzStr); + } + + /** + * 获取Bean的属性 + * + * @param bean bean + * @param propertyName 属性名 + * @return 属性值 + */ + @Nullable + public static Object getProperty(@Nullable Object bean, String propertyName) { + return BeanUtil.getProperty(bean, propertyName); + } + + /** + * 设置Bean属性 + * + * @param bean bean + * @param propertyName 属性名 + * @param value 属性值 + */ + public static void setProperty(Object bean, String propertyName, Object value) { + BeanUtil.setProperty(bean, propertyName, value); + } + + /** + * 浅复制 + * + * @param source 源对象 + * @param 泛型标记 + * @return T + */ + @Nullable + public static T clone(@Nullable T source) { + return BeanUtil.clone(source); + } + + /** + * 拷贝对象,支持 Map 和 Bean + * + * @param source 源对象 + * @param clazz 类名 + * @param 泛型标记 + * @return T + */ + @Nullable + public static T copy(@Nullable Object source, Class clazz) { + return BeanUtil.copy(source, clazz); + } + + /** + * 拷贝对象,支持 Map 和 Bean + * + * @param source 源对象 + * @param targetBean 需要赋值的对象 + */ + public static void copy(@Nullable Object source, @Nullable Object targetBean) { + BeanUtil.copy(source, targetBean); + } + + /** + * 拷贝对象,source 对象属性做非 null 判断 + * + *

+ * 支持 map bean copy + *

+ * + * @param source 源对象 + * @param targetBean 需要赋值的对象 + */ + public static void copyNonNull(@Nullable Object source, @Nullable Object targetBean) { + BeanUtil.copyNonNull(source, targetBean); + } + + /** + * 拷贝对象,并对不同类型属性进行转换 + * + * @param source 源对象 + * @param clazz 类名 + * @param 泛型标记 + * @return T + */ + @Nullable + public static T copyWithConvert(@Nullable Object source, Class clazz) { + return BeanUtil.copyWithConvert(source, clazz); + } + + /** + * 拷贝列表对象 + * + *

+ * 支持 map bean copy + *

+ * + * @param sourceList 源列表 + * @param targetClazz 转换成的类型 + * @param 泛型标记 + * @return T + */ + public static List copy(@Nullable Collection sourceList, Class targetClazz) { + return BeanUtil.copy(sourceList, targetClazz); + } + + /** + * 拷贝列表对象,并对不同类型属性进行转换 + * + *

+ * 支持 map bean copy + *

+ * + * @param sourceList 源对象列表 + * @param targetClazz 转换成的类 + * @param 泛型标记 + * @return List + */ + public static List copyWithConvert(@Nullable Collection sourceList, Class targetClazz) { + return BeanUtil.copyWithConvert(sourceList, targetClazz); + } + + /** + * 拷贝对象,扩展 Spring 的拷贝方法 + * + * @param source the source bean + * @param clazz the target bean class + * @param 泛型标记 + * @return T + * @throws BeansException if the copying failed + */ + @Nullable + public static T copyProperties(@Nullable Object source, Class clazz) throws BeansException { + return BeanUtil.copyProperties(source, clazz); + } + + /** + * 拷贝列表对象,扩展 Spring 的拷贝方法 + * + * @param sourceList the source list bean + * @param targetClazz the target bean class + * @param 泛型标记 + * @return List + * @throws BeansException if the copying failed + */ + public static List copyProperties(@Nullable Collection sourceList, Class targetClazz) throws BeansException { + return BeanUtil.copyProperties(sourceList, targetClazz); + } + + /** + * 将对象装成map形式 + * + * @param bean 源对象 + * @return {Map} + */ + public static Map toMap(@Nullable Object bean) { + return BeanUtil.toMap(bean); + } + + /** + * 将map 转为 bean + * + * @param beanMap map + * @param valueType 对象类型 + * @param 泛型标记 + * @return {T} + */ + public static T toBean(Map beanMap, Class valueType) { + return BeanUtil.toBean(beanMap, valueType); + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/HexUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/HexUtil.java new file mode 100644 index 0000000..d4acf2d --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/HexUtil.java @@ -0,0 +1,204 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.tool.utils; + +import org.springframework.lang.Nullable; + +import java.nio.charset.Charset; + +/** + * hex 工具,编解码全用 byte + * + * @author L.cm + */ +public class HexUtil { + public static final Charset DEFAULT_CHARSET = Charsets.UTF_8; + private static final byte[] DIGITS_LOWER = new byte[]{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; + private static final byte[] DIGITS_UPPER = new byte[]{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; + + /** + * encode Hex + * + * @param data data to hex + * @return hex bytes + */ + public static byte[] encode(byte[] data) { + return encode(data, true); + } + + /** + * encode Hex + * + * @param data data to hex + * @param toLowerCase 是否小写 + * @return hex bytes + */ + public static byte[] encode(byte[] data, boolean toLowerCase) { + return encode(data, toLowerCase ? DIGITS_LOWER : DIGITS_UPPER); + } + + /** + * encode Hex + * + * @param data Data to Hex + * @return bytes as a hex string + */ + private static byte[] encode(byte[] data, byte[] digits) { + int len = data.length; + byte[] out = new byte[len << 1]; + for (int i = 0, j = 0; i < len; i++) { + out[j++] = digits[(0xF0 & data[i]) >>> 4]; + out[j++] = digits[0xF & data[i]]; + } + return out; + } + + /** + * encode Hex + * + * @param data Data to Hex + * @param toLowerCase 是否小写 + * @return bytes as a hex string + */ + public static String encodeToString(byte[] data, boolean toLowerCase) { + return new String(encode(data, toLowerCase), DEFAULT_CHARSET); + } + + /** + * encode Hex + * + * @param data Data to Hex + * @return bytes as a hex string + */ + public static String encodeToString(byte[] data) { + return encodeToString(data, DEFAULT_CHARSET); + } + + /** + * encode Hex + * + * @param data Data to Hex + * @param charset Charset + * @return bytes as a hex string + */ + public static String encodeToString(byte[] data, Charset charset) { + return new String(encode(data), charset); + } + + /** + * encode Hex + * + * @param data Data to Hex + * @return bytes as a hex string + */ + @Nullable + public static String encodeToString(@Nullable String data) { + if (StringUtil.isBlank(data)) { + return null; + } + return encodeToString(data.getBytes(DEFAULT_CHARSET)); + } + + /** + * decode Hex + * + * @param data Hex data + * @return decode hex to bytes + */ + public static byte[] decode(String data) { + return decode(data, DEFAULT_CHARSET); + } + + /** + * decode Hex + * + * @param data Hex data + * @param charset Charset + * @return decode hex to bytes + */ + public static byte[] decode(String data, Charset charset) { + if (StringUtil.isBlank(data)) { + return null; + } + return decode(data.getBytes(charset)); + } + + /** + * decodeToString Hex + * + * @param data Data to Hex + * @return bytes as a hex string + */ + public static String decodeToString(byte[] data) { + byte[] decodeBytes = decode(data); + return new String(decodeBytes, DEFAULT_CHARSET); + } + + /** + * decodeToString Hex + * + * @param data Data to Hex + * @return bytes as a hex string + */ + @Nullable + public static String decodeToString(@Nullable String data) { + if (StringUtil.isBlank(data)) { + return null; + } + return decodeToString(data.getBytes(DEFAULT_CHARSET)); + } + + /** + * decode Hex + * + * @param data Hex data + * @return decode hex to bytes + */ + public static byte[] decode(byte[] data) { + int len = data.length; + if ((len & 0x01) != 0) { + throw new IllegalArgumentException("hexBinary needs to be even-length: " + len); + } + byte[] out = new byte[len >> 1]; + for (int i = 0, j = 0; j < len; i++) { + int f = toDigit(data[j], j) << 4; + j++; + f |= toDigit(data[j], j); + j++; + out[i] = (byte) (f & 0xFF); + } + return out; + } + + private static int toDigit(byte b, int index) { + int digit = Character.digit(b, 16); + if (digit == -1) { + throw new IllegalArgumentException("Illegal hexadecimal byte " + b + " at index " + index); + } + return digit; + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/Holder.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/Holder.java new file mode 100644 index 0000000..28e0b0f --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/Holder.java @@ -0,0 +1,48 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.tool.utils; + +import java.security.SecureRandom; +import java.util.Random; + +/** + * 一些常用的单利对象 + * + * @author L.cm + */ +public class Holder { + + /** + * RANDOM + */ + public final static Random RANDOM = new Random(); + + /** + * SECURE_RANDOM + */ + public final static SecureRandom SECURE_RANDOM = new SecureRandom(); +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/ImageUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/ImageUtil.java new file mode 100644 index 0000000..712ebf0 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/ImageUtil.java @@ -0,0 +1,498 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.tool.utils; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springblade.core.tool.support.IMultiOutputStream; +import org.springblade.core.tool.support.ImagePosition; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.color.ColorSpace; +import java.awt.geom.AffineTransform; +import java.awt.image.*; +import java.io.*; +import java.net.URL; + +/** + * 图片工具类 + * + * @author Chill + */ +public final class ImageUtil { + + /** + * Logger for this class + */ + private static Logger LOGGER = LoggerFactory.getLogger(ImageUtil.class); + + /** + * 默认输出图片类型 + */ + public static final String DEFAULT_IMG_TYPE = "JPEG"; + + private ImageUtil() { + + } + + /** + * 转换输入流到byte + * + * @param src 源 + * @param type 类型 + * @return byte[] + * @throws IOException 异常 + */ + public static byte[] toByteArray(BufferedImage src, String type) throws IOException { + ByteArrayOutputStream os = new ByteArrayOutputStream(); + ImageIO.write(src, defaultString(type, DEFAULT_IMG_TYPE), os); + return os.toByteArray(); + } + + /** + * 获取图像内容 + * + * @param srcImageFile 文件路径 + * @return BufferedImage + */ + public static BufferedImage readImage(String srcImageFile) { + try { + return ImageIO.read(new File(srcImageFile)); + } catch (IOException e) { + LOGGER.error("Error readImage", e); + } + return null; + } + + /** + * 获取图像内容 + * + * @param srcImageFile 文件 + * @return BufferedImage + */ + public static BufferedImage readImage(File srcImageFile) { + try { + return ImageIO.read(srcImageFile); + } catch (IOException e) { + LOGGER.error("Error readImage", e); + } + return null; + } + + /** + * 获取图像内容 + * + * @param srcInputStream 输入流 + * @return BufferedImage + */ + public static BufferedImage readImage(InputStream srcInputStream) { + try { + return ImageIO.read(srcInputStream); + } catch (IOException e) { + LOGGER.error("Error readImage", e); + } + return null; + } + + /** + * 获取图像内容 + * + * @param url URL地址 + * @return BufferedImage + */ + public static BufferedImage readImage(URL url) { + try { + return ImageIO.read(url); + } catch (IOException e) { + LOGGER.error("Error readImage", e); + } + return null; + } + + + /** + * 缩放图像(按比例缩放) + * + * @param src 源图像 + * @param output 输出流 + * @param type 类型 + * @param scale 缩放比例 + * @param flag 缩放选择:true 放大; false 缩小; + */ + public final static void zoomScale(BufferedImage src, OutputStream output, String type, double scale, boolean flag) { + try { + // 得到源图宽 + int width = src.getWidth(); + // 得到源图长 + int height = src.getHeight(); + if (flag) { + // 放大 + width = Long.valueOf(Math.round(width * scale)).intValue(); + height = Long.valueOf(Math.round(height * scale)).intValue(); + } else { + // 缩小 + width = Long.valueOf(Math.round(width / scale)).intValue(); + height = Long.valueOf(Math.round(height / scale)).intValue(); + } + Image image = src.getScaledInstance(width, height, Image.SCALE_DEFAULT); + BufferedImage tag = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + Graphics g = tag.getGraphics(); + + g.drawImage(image, 0, 0, null); + g.dispose(); + + ImageIO.write(tag, defaultString(type, DEFAULT_IMG_TYPE), output); + + output.close(); + } catch (IOException e) { + LOGGER.error("Error in zoom image", e); + } + } + + /** + * 缩放图像(按高度和宽度缩放) + * + * @param src 源图像 + * @param output 输出流 + * @param type 类型 + * @param height 缩放后的高度 + * @param width 缩放后的宽度 + * @param bb 比例不对时是否需要补白:true为补白; false为不补白; + * @param fillColor 填充色,null时为Color.WHITE + */ + public final static void zoomFixed(BufferedImage src, OutputStream output, String type, int height, int width, boolean bb, Color fillColor) { + try { + double ratio = 0.0; + Image itemp = src.getScaledInstance(width, height, BufferedImage.SCALE_SMOOTH); + // 计算比例 + if (src.getHeight() > src.getWidth()) { + ratio = Integer.valueOf(height).doubleValue() / src.getHeight(); + } else { + ratio = Integer.valueOf(width).doubleValue() / src.getWidth(); + } + AffineTransformOp op = new AffineTransformOp(AffineTransform.getScaleInstance(ratio, ratio), null); + itemp = op.filter(src, null); + + if (bb) { + //补白 + BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + Graphics2D g = image.createGraphics(); + Color fill = fillColor == null ? Color.white : fillColor; + g.setColor(fill); + g.fillRect(0, 0, width, height); + if (width == itemp.getWidth(null)) { + g.drawImage(itemp, 0, (height - itemp.getHeight(null)) / 2, itemp.getWidth(null), itemp.getHeight(null), fill, null); + } else { + g.drawImage(itemp, (width - itemp.getWidth(null)) / 2, 0, itemp.getWidth(null), itemp.getHeight(null), fill, null); + } + g.dispose(); + itemp = image; + } + // 输出为文件 + ImageIO.write((BufferedImage) itemp, defaultString(type, DEFAULT_IMG_TYPE), output); + // 关闭流 + output.close(); + } catch (IOException e) { + LOGGER.error("Error in zoom image", e); + } + } + + /** + * 图像裁剪(按指定起点坐标和宽高切割) + * + * @param src 源图像 + * @param output 切片后的图像地址 + * @param type 类型 + * @param x 目标切片起点坐标X + * @param y 目标切片起点坐标Y + * @param width 目标切片宽度 + * @param height 目标切片高度 + */ + public final static void crop(BufferedImage src, OutputStream output, String type, int x, int y, int width, int height) { + try { + // 源图宽度 + int srcWidth = src.getWidth(); + // 源图高度 + int srcHeight = src.getHeight(); + if (srcWidth > 0 && srcHeight > 0) { + Image image = src.getScaledInstance(srcWidth, srcHeight, Image.SCALE_DEFAULT); + // 四个参数分别为图像起点坐标和宽高 + ImageFilter cropFilter = new CropImageFilter(x, y, width, height); + Image img = Toolkit.getDefaultToolkit().createImage(new FilteredImageSource(image.getSource(), cropFilter)); + BufferedImage tag = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + Graphics g = tag.getGraphics(); + g.drawImage(img, 0, 0, width, height, null); + g.dispose(); + // 输出为文件 + ImageIO.write(tag, defaultString(type, DEFAULT_IMG_TYPE), output); + // 关闭流 + output.close(); + } + } catch (Exception e) { + LOGGER.error("Error in cut image", e); + } + } + + /** + * 图像切割(指定切片的行数和列数) + * + * @param src 源图像地址 + * @param mos 切片目标文件夹 + * @param type 类型 + * @param prows 目标切片行数。默认2,必须是范围 [1, 20] 之内 + * @param pcols 目标切片列数。默认2,必须是范围 [1, 20] 之内 + */ + public final static void sliceWithNumber(BufferedImage src, IMultiOutputStream mos, String type, int prows, int pcols) { + try { + int rows = prows <= 0 || prows > 20 ? 2 : prows; + int cols = pcols <= 0 || pcols > 20 ? 2 : pcols; + // 源图宽度 + int srcWidth = src.getWidth(); + // 源图高度 + int srcHeight = src.getHeight(); + if (srcWidth > 0 && srcHeight > 0) { + Image img; + ImageFilter cropFilter; + Image image = src.getScaledInstance(srcWidth, srcHeight, Image.SCALE_DEFAULT); + // 每张切片的宽度 + int destWidth = (srcWidth % cols == 0) ? (srcWidth / cols) : (srcWidth / cols + 1); + // 每张切片的高度 + int destHeight = (srcHeight % rows == 0) ? (srcHeight / rows) : (srcHeight / rows + 1); + // 循环建立切片 + // 改进的想法:是否可用多线程加快切割速度 + for (int i = 0; i < rows; i++) { + for (int j = 0; j < cols; j++) { + // 四个参数分别为图像起点坐标和宽高 + cropFilter = new CropImageFilter(j * destWidth, i * destHeight, destWidth, destHeight); + img = Toolkit.getDefaultToolkit().createImage(new FilteredImageSource(image.getSource(), cropFilter)); + BufferedImage tag = new BufferedImage(destWidth, destHeight, BufferedImage.TYPE_INT_RGB); + Graphics g = tag.getGraphics(); + // 绘制缩小后的图 + g.drawImage(img, 0, 0, null); + g.dispose(); + // 输出为文件 + ImageIO.write(tag, defaultString(type, DEFAULT_IMG_TYPE), mos.buildOutputStream(i, j)); + } + } + } + } catch (Exception e) { + LOGGER.error("Error in slice image", e); + } + } + + /** + * 图像切割(指定切片的宽度和高度) + * + * @param src 源图像地址 + * @param mos 切片目标文件夹 + * @param type 类型 + * @param pdestWidth 目标切片宽度。默认200 + * @param pdestHeight 目标切片高度。默认150 + */ + public final static void sliceWithSize(BufferedImage src, IMultiOutputStream mos, String type, int pdestWidth, int pdestHeight) { + try { + int destWidth = pdestWidth <= 0 ? 200 : pdestWidth; + int destHeight = pdestHeight <= 0 ? 150 : pdestHeight; + // 源图宽度 + int srcWidth = src.getWidth(); + // 源图高度 + int srcHeight = src.getHeight(); + if (srcWidth > destWidth && srcHeight > destHeight) { + Image img; + ImageFilter cropFilter; + Image image = src.getScaledInstance(srcWidth, srcHeight, Image.SCALE_DEFAULT); + // 切片横向数量 + int cols = (srcWidth % destWidth == 0) ? (srcWidth / destWidth) : (srcWidth / destWidth + 1); + // 切片纵向数量 + int rows = (srcHeight % destHeight == 0) ? (srcHeight / destHeight) : (srcHeight / destHeight + 1); + // 循环建立切片 + // 改进的想法:是否可用多线程加快切割速度 + for (int i = 0; i < rows; i++) { + for (int j = 0; j < cols; j++) { + // 四个参数分别为图像起点坐标和宽高 + cropFilter = new CropImageFilter(j * destWidth, i * destHeight, destWidth, destHeight); + img = Toolkit.getDefaultToolkit().createImage(new FilteredImageSource(image.getSource(), cropFilter)); + BufferedImage tag = new BufferedImage(destWidth, destHeight, BufferedImage.TYPE_INT_RGB); + Graphics g = tag.getGraphics(); + // 绘制缩小后的图 + g.drawImage(img, 0, 0, null); + g.dispose(); + // 输出为文件 + ImageIO.write(tag, defaultString(type, DEFAULT_IMG_TYPE), mos.buildOutputStream(i, j)); + } + } + } + } catch (Exception e) { + LOGGER.error("Error in slice image", e); + } + } + + /** + * 图像类型转换:GIF-JPG、GIF-PNG、PNG-JPG、PNG-GIF(X)、BMP-PNG + * + * @param src 源图像地址 + * @param formatName 包含格式非正式名称的 String:如JPG、JPEG、GIF等 + * @param output 目标图像地址 + */ + public final static void convert(BufferedImage src, OutputStream output, String formatName) { + try { + // 输出为文件 + ImageIO.write(src, formatName, output); + // 关闭流 + output.close(); + } catch (Exception e) { + LOGGER.error("Error in convert image", e); + } + } + + /** + * 彩色转为黑白 + * + * @param src 源图像地址 + * @param output 目标图像地址 + * @param type 类型 + */ + public final static void gray(BufferedImage src, OutputStream output, String type) { + try { + ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_GRAY); + ColorConvertOp op = new ColorConvertOp(cs, null); + src = op.filter(src, null); + // 输出为文件 + ImageIO.write(src, defaultString(type, DEFAULT_IMG_TYPE), output); + // 关闭流 + output.close(); + } catch (IOException e) { + LOGGER.error("Error in gray image", e); + } + } + + /** + * 给图片添加文字水印 + * + * @param src 源图像 + * @param output 输出流 + * @param type 类型 + * @param text 水印文字 + * @param font 水印的字体 + * @param color 水印的字体颜色 + * @param position 水印位置 {@link ImagePosition} + * @param x 修正值 + * @param y 修正值 + * @param alpha 透明度:alpha 必须是范围 [0.0, 1.0] 之内(包含边界值)的一个浮点数字 + */ + public final static void textStamp(BufferedImage src, OutputStream output, String type, String text, Font font, Color color + , int position, int x, int y, float alpha) { + try { + int width = src.getWidth(null); + int height = src.getHeight(null); + BufferedImage image = new BufferedImage(width, height, + BufferedImage.TYPE_INT_RGB); + Graphics2D g = image.createGraphics(); + g.drawImage(src, 0, 0, width, height, null); + g.setColor(color); + g.setFont(font); + g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, alpha)); + // 在指定坐标绘制水印文字 + ImagePosition boxPos = new ImagePosition(width, height, calcTextWidth(text) * font.getSize(), font.getSize(), position); + g.drawString(text, boxPos.getX(x), boxPos.getY(y)); + g.dispose(); + // 输出为文件 + ImageIO.write((BufferedImage) image, defaultString(type, DEFAULT_IMG_TYPE), output); + // 关闭流 + output.close(); + } catch (Exception e) { + LOGGER.error("Error in textStamp image", e); + } + } + + /** + * 给图片添加图片水印 + * + * @param src 源图像 + * @param output 输出流 + * @param type 类型 + * @param stamp 水印图片 + * @param position 水印位置 {@link ImagePosition} + * @param x 修正值 + * @param y 修正值 + * @param alpha 透明度:alpha 必须是范围 [0.0, 1.0] 之内(包含边界值)的一个浮点数字 + */ + public final static void imageStamp(BufferedImage src, OutputStream output, String type, BufferedImage stamp + , int position, int x, int y, float alpha) { + try { + int width = src.getWidth(); + int height = src.getHeight(); + BufferedImage image = new BufferedImage(width, height, + BufferedImage.TYPE_INT_RGB); + Graphics2D g = image.createGraphics(); + g.drawImage(src, 0, 0, width, height, null); + // 水印文件 + int stampWidth = stamp.getWidth(); + int stampHeight = stamp.getHeight(); + g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, alpha)); + ImagePosition boxPos = new ImagePosition(width, height, stampWidth, stampHeight, position); + g.drawImage(stamp, boxPos.getX(x), boxPos.getY(y), stampWidth, stampHeight, null); + // 水印文件结束 + g.dispose(); + // 输出为文件 + ImageIO.write((BufferedImage) image, defaultString(type, DEFAULT_IMG_TYPE), output); + // 关闭流 + output.close(); + } catch (Exception e) { + LOGGER.error("Error imageStamp", e); + } + } + + /** + * 计算text的长度(一个中文算两个字符) + * + * @param text text + * @return int + */ + public final static int calcTextWidth(String text) { + int length = 0; + for (int i = 0; i < text.length(); i++) { + if (new String(text.charAt(i) + "").getBytes().length > 1) { + length += 2; + } else { + length += 1; + } + } + return length / 2; + } + + /** + * 默认字符串 + * @param str 字符串 + * @param defaultStr 默认值 + * @return + */ + public static String defaultString(String str, String defaultStr) { + return ((str == null) ? defaultStr : str); + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/IntegerPool.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/IntegerPool.java new file mode 100644 index 0000000..0764aab --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/IntegerPool.java @@ -0,0 +1,37 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.tool.utils; + +/** + * 静态 Integer 池. + * + * @author Chill + */ +public interface IntegerPool { + + Integer INT_1024 = 1024; + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/IoUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/IoUtil.java new file mode 100644 index 0000000..a9c4e4e --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/IoUtil.java @@ -0,0 +1,118 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.tool.utils; + +import org.springframework.lang.Nullable; + +import java.io.*; +import java.nio.charset.Charset; + +/** + * 流工具类 + * + * @author L.cm + */ +public class IoUtil extends org.springframework.util.StreamUtils { + + /** + * closeQuietly + * + * @param closeable 自动关闭 + */ + public static void closeQuietly(@Nullable Closeable closeable) { + if (closeable == null) { + return; + } + if (closeable instanceof Flushable) { + try { + ((Flushable) closeable).flush(); + } catch (IOException ignored) { + // ignore + } + } + try { + closeable.close(); + } catch (IOException ignored) { + // ignore + } + } + + /** + * InputStream to String utf-8 + * + * @param input the InputStream to read from + * @return the requested String + */ + public static String readToString(InputStream input) { + return readToString(input, Charsets.UTF_8); + } + + /** + * InputStream to String + * + * @param input the InputStream to read from + * @param charset the Charset + * @return the requested String + */ + public static String readToString(@Nullable InputStream input, Charset charset) { + try { + return IoUtil.copyToString(input, charset); + } catch (IOException e) { + throw Exceptions.unchecked(e); + } finally { + IoUtil.closeQuietly(input); + } + } + + public static byte[] readToByteArray(@Nullable InputStream input) { + try { + return IoUtil.copyToByteArray(input); + } catch (IOException e) { + throw Exceptions.unchecked(e); + } finally { + IoUtil.closeQuietly(input); + } + } + + /** + * Writes chars from a String to bytes on an + * OutputStream using the specified character encoding. + *

+ * This method uses {@link String#getBytes(String)}. + *

+ * @param data the String to write, null ignored + * @param output the OutputStream to write to + * @param encoding the encoding to use, null means platform default + * @throws NullPointerException if output is null + * @throws IOException if an I/O error occurs + */ + public static void write(@Nullable final String data, final OutputStream output, final Charset encoding) throws IOException { + if (data != null) { + output.write(data.getBytes(encoding)); + } + } +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/Lazy.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/Lazy.java new file mode 100644 index 0000000..6ee67df --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/Lazy.java @@ -0,0 +1,81 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.tool.utils; + +import org.springframework.lang.Nullable; + +import java.io.Serializable; +import java.util.function.Supplier; + +/** + * Holder of a value that is computed lazy. + * + * @author L.cm + */ +public class Lazy implements Supplier, Serializable { + + @Nullable + private transient volatile Supplier supplier; + @Nullable + private T value; + + /** + * Creates new instance of Lazy. + * + * @param supplier Supplier + * @param 泛型标记 + * @return Lazy + */ + public static Lazy of(final Supplier supplier) { + return new Lazy<>(supplier); + } + + private Lazy(final Supplier supplier) { + this.supplier = supplier; + } + + /** + * Returns the value. Value will be computed on first call. + * + * @return lazy value + */ + @Nullable + @Override + public T get() { + return (supplier == null) ? value : computeValue(); + } + + @Nullable + private synchronized T computeValue() { + final Supplier s = supplier; + if (s != null) { + value = s.get(); + supplier = null; + } + return value; + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/MultipartUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/MultipartUtil.java new file mode 100644 index 0000000..b89d574 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/MultipartUtil.java @@ -0,0 +1,66 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.tool.utils; + +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.multipart.MultipartHttpServletRequest; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * MultipartUtils + * + * @author Chill + */ +public class MultipartUtil { + /** + * 从HttpServletRequest中解析并返回所有的MultipartFile + * + * @param request HttpServletRequest对象,应为MultipartHttpServletRequest类型 + * @return 包含所有MultipartFile的列表,如果没有文件或请求不是多部分请求,则返回空列表 + */ + public static List extractMultipartFiles(HttpServletRequest request) { + List files = new ArrayList<>(); + + if (request instanceof MultipartHttpServletRequest multipartRequest) { + + // 获取所有文件的映射 + Map fileMap = multipartRequest.getFileMap(); + + // 遍历映射,并将所有MultipartFile添加到列表中 + for (MultipartFile file : fileMap.values()) { + if (file != null && !file.isEmpty()) { + files.add(file); + } + } + } + + return files; + } +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/NumberUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/NumberUtil.java new file mode 100644 index 0000000..9d89584 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/NumberUtil.java @@ -0,0 +1,248 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.tool.utils; + + +import org.springframework.lang.Nullable; + +/** + * 数字类型工具类 + * + * @author L.cm + */ +public class NumberUtil extends org.springframework.util.NumberUtils { + + //----------------------------------------------------------------------- + + /** + *

Convert a String to an int, returning + * zero if the conversion fails.

+ * + *

If the string is null, zero is returned.

+ * + *
+	 *   NumberUtil.toInt(null) = 0
+	 *   NumberUtil.toInt("")   = 0
+	 *   NumberUtil.toInt("1")  = 1
+	 * 
+ * + * @param str the string to convert, may be null + * @return the int represented by the string, or zero if + * conversion fails + */ + public static int toInt(final String str) { + return toInt(str, -1); + } + + /** + *

Convert a String to an int, returning a + * default value if the conversion fails.

+ * + *

If the string is null, the default value is returned.

+ * + *
+	 *   NumberUtil.toInt(null, 1) = 1
+	 *   NumberUtil.toInt("", 1)   = 1
+	 *   NumberUtil.toInt("1", 0)  = 1
+	 * 
+ * + * @param str the string to convert, may be null + * @param defaultValue the default value + * @return the int represented by the string, or the default if conversion fails + */ + public static int toInt(@Nullable final String str, final int defaultValue) { + if (str == null) { + return defaultValue; + } + try { + return Integer.valueOf(str); + } catch (final NumberFormatException nfe) { + return defaultValue; + } + } + + /** + *

Convert a String to a long, returning + * zero if the conversion fails.

+ * + *

If the string is null, zero is returned.

+ * + *
+	 *   NumberUtil.toLong(null) = 0L
+	 *   NumberUtil.toLong("")   = 0L
+	 *   NumberUtil.toLong("1")  = 1L
+	 * 
+ * + * @param str the string to convert, may be null + * @return the long represented by the string, or 0 if + * conversion fails + */ + public static long toLong(final String str) { + return toLong(str, 0L); + } + + /** + *

Convert a String to a long, returning a + * default value if the conversion fails.

+ * + *

If the string is null, the default value is returned.

+ * + *
+	 *   NumberUtil.toLong(null, 1L) = 1L
+	 *   NumberUtil.toLong("", 1L)   = 1L
+	 *   NumberUtil.toLong("1", 0L)  = 1L
+	 * 
+ * + * @param str the string to convert, may be null + * @param defaultValue the default value + * @return the long represented by the string, or the default if conversion fails + */ + public static long toLong(@Nullable final String str, final long defaultValue) { + if (str == null) { + return defaultValue; + } + try { + return Long.valueOf(str); + } catch (final NumberFormatException nfe) { + return defaultValue; + } + } + + /** + *

Convert a String to a Double + * + * @param value value + * @return double value + */ + public static Double toDouble(String value) { + return toDouble(value, null); + } + + /** + *

Convert a String to a Double + * + * @param value value + * @param defaultValue 默认值 + * @return double value + */ + public static Double toDouble(@Nullable String value, Double defaultValue) { + if (value != null) { + return Double.valueOf(value.trim()); + } + return defaultValue; + } + + /** + *

Convert a String to a Double + * + * @param value value + * @return double value + */ + public static Float toFloat(String value) { + return toFloat(value, null); + } + + /** + *

Convert a String to a Double + * + * @param value value + * @param defaultValue 默认值 + * @return double value + */ + public static Float toFloat(@Nullable String value, Float defaultValue) { + if (value != null) { + return Float.valueOf(value.trim()); + } + return defaultValue; + } + + /** + * All possible chars for representing a number as a String + */ + private final static char[] DIGITS = { + '0', '1', '2', '3', '4', '5', + '6', '7', '8', '9', 'a', 'b', + 'c', 'd', 'e', 'f', 'g', 'h', + 'i', 'j', 'k', 'l', 'm', 'n', + 'o', 'p', 'q', 'r', 's', 't', + 'u', 'v', 'w', 'x', 'y', 'z', + 'A', 'B', 'C', 'D', 'E', 'F', + 'G', 'H', 'I', 'J', 'K', 'L', + 'M', 'N', 'O', 'P', 'Q', 'R', + 'S', 'T', 'U', 'V', 'W', 'X', + 'Y', 'Z' + }; + + /** + * 将 long 转短字符串 为 62 进制 + * + * @param i 数字 + * @return 短字符串 + */ + public static String to62String(long i) { + int radix = DIGITS.length; + char[] buf = new char[65]; + int charPos = 64; + i = -i; + while (i <= -radix) { + buf[charPos--] = DIGITS[(int) (-(i % radix))]; + i = i / radix; + } + buf[charPos] = DIGITS[(int) (-i)]; + + return new String(buf, charPos, (65 - charPos)); + } + + /** + * 根据指定的分隔符分割字符串并尝试解析为整数。 + * + * @param str 需要分割和解析的字符串 + * @return 分割后数组的第一个元素转换成的整数,或者在无法解析时返回0 + */ + public static int parseFirstInt(String str) { + // 定义分隔符数组 + String[] delimiters = {"-", ":", ","}; + + // 对每个分隔符进行处理 + for (String delimiter : delimiters) { + String[] parts = str.split(delimiter); + + // 如果分割后数组长度大于2,尝试解析第一个元素 + if (parts.length >= 2) { + try { + return Integer.parseInt(parts[0].trim()); + } catch (NumberFormatException e) { + // 如果解析失败,返回0 + return 999; + } + } + } + + // 如果没有分隔符或者分割后数组长度不足2,返回999 + return 999; + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/ObjectUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/ObjectUtil.java new file mode 100644 index 0000000..ae1a8bf --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/ObjectUtil.java @@ -0,0 +1,46 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.tool.utils; + +import org.springframework.lang.Nullable; + +/** + * 对象工具类 + * + * @author L.cm + */ +public class ObjectUtil extends org.springframework.util.ObjectUtils { + + /** + * 判断元素不为空 + * @param obj object + * @return boolean + */ + public static boolean isNotEmpty(@Nullable Object obj) { + return !ObjectUtil.isEmpty(obj); + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/PathUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/PathUtil.java new file mode 100644 index 0000000..3dcfe74 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/PathUtil.java @@ -0,0 +1,64 @@ +package org.springblade.core.tool.utils; + +import org.springframework.lang.Nullable; + +import java.io.File; +import java.net.URL; + +/** + * 用来获取各种目录 + * + * @author L.cm + */ +public class PathUtil { + public static final String FILE_PROTOCOL = "file"; + public static final String JAR_PROTOCOL = "jar"; + public static final String ZIP_PROTOCOL = "zip"; + public static final String FILE_PROTOCOL_PREFIX = "file:"; + public static final String JAR_FILE_SEPARATOR = "!/"; + + /** + * 获取jar包运行时的当前目录 + * + * @return {String} + */ + @Nullable + public static String getJarPath() { + try { + URL url = PathUtil.class.getResource(StringPool.SLASH).toURI().toURL(); + return PathUtil.toFilePath(url); + } catch (Exception e) { + String path = PathUtil.class.getResource(StringPool.EMPTY).getPath(); + return new File(path).getParentFile().getParentFile().getAbsolutePath(); + } + } + + /** + * 转换为文件路径 + * + * @param url 路径 + * @return {String} + */ + @Nullable + public static String toFilePath(@Nullable URL url) { + if (url == null) { + return null; + } + String protocol = url.getProtocol(); + String file = UrlUtil.decode(url.getPath(), Charsets.UTF_8); + if (FILE_PROTOCOL.equals(protocol)) { + return new File(file).getParentFile().getParentFile().getAbsolutePath(); + } else if (JAR_PROTOCOL.equals(protocol) || ZIP_PROTOCOL.equals(protocol)) { + int ipos = file.indexOf(JAR_FILE_SEPARATOR); + if (ipos > 0) { + file = file.substring(0, ipos); + } + if (file.startsWith(FILE_PROTOCOL_PREFIX)) { + file = file.substring(FILE_PROTOCOL_PREFIX.length()); + } + return new File(file).getParentFile().getAbsolutePath(); + } + return file; + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/Pkcs7Encoder.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/Pkcs7Encoder.java new file mode 100644 index 0000000..024277d --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/Pkcs7Encoder.java @@ -0,0 +1,61 @@ +package org.springblade.core.tool.utils; + +import java.util.Arrays; + +/** + * 提供基于 PKCS7 算法的加解密接口. + *

+ * 参考自:jFinal 方便使用 + * + * @author L.cm + */ +public class Pkcs7Encoder { + /** + * 默认为 16,保持跟其他语言的一致性 + */ + private static final int BLOCK_SIZE = 16; + + /** + * PKCS7 编码 padding 补位 + * + * @param src 原数据 + * @return padding 补位 + */ + public static byte[] encode(byte[] src) { + int count = src.length; + // 计算需要填充的位数 + int amountToPad = BLOCK_SIZE - (count % BLOCK_SIZE); + if (amountToPad == 0) { + amountToPad = BLOCK_SIZE; + } + // 获得补位所用的字符 + byte pad = (byte) (amountToPad & 0xFF); + byte[] pads = new byte[amountToPad]; + for (int index = 0; index < amountToPad; index++) { + pads[index] = pad; + } + int length = count + amountToPad; + byte[] dest = new byte[length]; + System.arraycopy(src, 0, dest, 0, count); + System.arraycopy(pads, 0, dest, count, amountToPad); + return dest; + } + + /** + * PKCS7 解码 + * + * @param decrypted 编码的数据 + * @return 解码后的数据 + */ + public static byte[] decode(byte[] decrypted) { + int pad = decrypted[decrypted.length - 1]; + if (pad < 1 || pad > BLOCK_SIZE) { + pad = 0; + } + if (pad > 0) { + return Arrays.copyOfRange(decrypted, 0, decrypted.length - pad); + } + return decrypted; + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/PlaceholderUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/PlaceholderUtil.java new file mode 100644 index 0000000..9c55108 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/PlaceholderUtil.java @@ -0,0 +1,152 @@ +package org.springblade.core.tool.utils; + +import java.util.Map; +import java.util.Properties; +import java.util.function.Function; +import java.util.stream.Stream; + +/** + * 占位符解析器 + * + * @author meilin.huang, chill + */ +public class PlaceholderUtil { + /** + * 默认前缀占位符 + */ + public static final String DEFAULT_PLACEHOLDER_PREFIX = "${"; + + /** + * 默认后缀占位符 + */ + public static final String DEFAULT_PLACEHOLDER_SUFFIX = "}"; + + /** + * 默认单例解析器 + */ + private static final PlaceholderUtil DEFAULT_RESOLVER = new PlaceholderUtil(); + + /** + * 占位符前缀 + */ + private String placeholderPrefix = DEFAULT_PLACEHOLDER_PREFIX; + + /** + * 占位符后缀 + */ + private String placeholderSuffix = DEFAULT_PLACEHOLDER_SUFFIX; + + + private PlaceholderUtil() { + } + + private PlaceholderUtil(String placeholderPrefix, String placeholderSuffix) { + this.placeholderPrefix = placeholderPrefix; + this.placeholderSuffix = placeholderSuffix; + } + + /** + * 获取默认的占位符解析器,即占位符前缀为"${", 后缀为"}" + * + * @return PlaceholderUtil + */ + public static PlaceholderUtil getDefaultResolver() { + return DEFAULT_RESOLVER; + } + + public static PlaceholderUtil getResolver(String placeholderPrefix, String placeholderSuffix) { + return new PlaceholderUtil(placeholderPrefix, placeholderSuffix); + } + + /** + * 解析带有指定占位符的模板字符串,默认占位符为前缀:${ 后缀:}

+ * 如:template = category:${}:product:${}
+ * values = {"1", "2"}
+ * 返回 category:1:product:2
+ * + * @param content 要解析的带有占位符的模板字符串 + * @param values 按照模板占位符索引位置设置对应的值 + * @return {String} + */ + public String resolve(String content, String... values) { + int start = content.indexOf(this.placeholderPrefix); + if (start == -1) { + return content; + } + //值索引 + int valueIndex = 0; + StringBuilder result = new StringBuilder(content); + while (start != -1) { + int end = result.indexOf(this.placeholderSuffix); + String replaceContent = values[valueIndex++]; + result.replace(start, end + this.placeholderSuffix.length(), replaceContent); + start = result.indexOf(this.placeholderPrefix, start + replaceContent.length()); + } + return result.toString(); + } + + /** + * 解析带有指定占位符的模板字符串,默认占位符为前缀:${ 后缀:}

+ * 如:template = category:${}:product:${}
+ * values = {"1", "2"}
+ * 返回 category:1:product:2
+ * + * @param content 要解析的带有占位符的模板字符串 + * @param values 按照模板占位符索引位置设置对应的值 + * @return {String} + */ + public String resolve(String content, Object[] values) { + return resolve(content, Stream.of(values).map(String::valueOf).toArray(String[]::new)); + } + + /** + * 根据替换规则来替换指定模板中的占位符值 + * + * @param content 要解析的字符串 + * @param rule 解析规则回调 + * @return {String} + */ + public String resolveByRule(String content, Function rule) { + int start = content.indexOf(this.placeholderPrefix); + if (start == -1) { + return content; + } + StringBuilder result = new StringBuilder(content); + while (start != -1) { + int end = result.indexOf(this.placeholderSuffix, start + 1); + //获取占位符属性值,如${id}, 即获取id + String placeholder = result.substring(start + this.placeholderPrefix.length(), end); + //替换整个占位符内容,即将${id}值替换为替换规则回调中的内容 + String replaceContent = placeholder.trim().isEmpty() ? "" : rule.apply(placeholder); + result.replace(start, end + this.placeholderSuffix.length(), replaceContent); + start = result.indexOf(this.placeholderPrefix, start + replaceContent.length()); + } + return result.toString(); + } + + /** + * 替换模板中占位符内容,占位符的内容即为map key对应的值,key为占位符中的内容。

+ * 如:content = product:${id}:detail:${did}
+ * valueMap = id -> 1; pid -> 2
+ * 经过解析返回 product:1:detail:2
+ * + * @param content 模板内容 + * @param valueMap 值映射 + * @return 替换完成后的字符串 + */ + public String resolveByMap(String content, final Map valueMap) { + return resolveByRule(content, placeholderValue -> String.valueOf(valueMap.get(placeholderValue))); + } + + /** + * 根据properties文件替换占位符内容 + * + * @param content 模板内容 + * @param properties 配置 + * @return {String} + */ + public String resolveByProperties(String content, final Properties properties) { + return resolveByRule(content, properties::getProperty); + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/ProtostuffUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/ProtostuffUtil.java new file mode 100644 index 0000000..21d936c --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/ProtostuffUtil.java @@ -0,0 +1,108 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.tool.utils; + +import io.protostuff.LinkedBuffer; +import io.protostuff.ProtobufIOUtil; +import io.protostuff.Schema; +import io.protostuff.runtime.RuntimeSchema; + +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Protostuff 工具类 + * + * @author L.cm + */ +public class ProtostuffUtil { + + /** + * 避免每次序列化都重新申请Buffer空间 + */ + private static final LinkedBuffer BUFFER = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE); + /** + * 缓存Schema + */ + private static final Map, Schema> SCHEMA_CACHE = new ConcurrentHashMap<>(); + + /** + * 序列化方法,把指定对象序列化成字节数组 + * + * @param obj obj + * @param T + * @return byte[] + */ + @SuppressWarnings("unchecked") + public static byte[] serialize(T obj) { + Class clazz = (Class) obj.getClass(); + Schema schema = getSchema(clazz); + byte[] data; + try { + data = ProtobufIOUtil.toByteArray(obj, schema, BUFFER); + } finally { + BUFFER.clear(); + } + return data; + } + + /** + * 反序列化方法,将字节数组反序列化成指定Class类型 + * + * @param data data + * @param clazz clazz + * @param T + * @return T + */ + public static T deserialize(byte[] data, Class clazz) { + Schema schema = getSchema(clazz); + T obj = schema.newMessage(); + ProtobufIOUtil.mergeFrom(data, obj, schema); + return obj; + } + + /** + * 获取Schema + * @param clazz clazz + * @param T + * @return T + */ + @SuppressWarnings("unchecked") + private static Schema getSchema(Class clazz) { + Schema schema = (Schema) SCHEMA_CACHE.get(clazz); + if (Objects.isNull(schema)) { + //这个schema通过RuntimeSchema进行懒创建并缓存 + //所以可以一直调用RuntimeSchema.getSchema(),这个方法是线程安全的 + schema = RuntimeSchema.getSchema(clazz); + if (Objects.nonNull(schema)) { + SCHEMA_CACHE.put(clazz, schema); + } + } + return schema; + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/RandomType.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/RandomType.java new file mode 100644 index 0000000..f4f78ff --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/RandomType.java @@ -0,0 +1,54 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.tool.utils; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * 生成的随机数类型 + * + * @author L.cm + */ +@Getter +@RequiredArgsConstructor +public enum RandomType { + /** + * INT STRING ALL + */ + INT(RandomType.INT_STR), + STRING(RandomType.STR_STR), + ALL(RandomType.ALL_STR); + + private final String factor; + + /** + * 随机字符串因子 + */ + private static final String INT_STR = "0123456789"; + private static final String STR_STR = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + private static final String ALL_STR = INT_STR + STR_STR; +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/ReflectUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/ReflectUtil.java new file mode 100644 index 0000000..62a4cf8 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/ReflectUtil.java @@ -0,0 +1,189 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.tool.utils; + +import org.springframework.beans.BeansException; +import org.springframework.cglib.core.CodeGenerationException; +import org.springframework.core.convert.Property; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.lang.Nullable; +import org.springframework.util.ReflectionUtils; + +import java.beans.PropertyDescriptor; +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +/** + * 反射工具类 + * + * @author L.cm + */ +public class ReflectUtil extends ReflectionUtils { + + /** + * 获取 Bean 的所有 get方法 + * + * @param type 类 + * @return PropertyDescriptor数组 + */ + public static PropertyDescriptor[] getBeanGetters(Class type) { + return getPropertiesHelper(type, true, false); + } + + /** + * 获取 Bean 的所有 set方法 + * + * @param type 类 + * @return PropertyDescriptor数组 + */ + public static PropertyDescriptor[] getBeanSetters(Class type) { + return getPropertiesHelper(type, false, true); + } + + /** + * 获取 Bean 的所有 PropertyDescriptor + * + * @param type 类 + * @param read 读取方法 + * @param write 写方法 + * @return PropertyDescriptor数组 + */ + public static PropertyDescriptor[] getPropertiesHelper(Class type, boolean read, boolean write) { + try { + PropertyDescriptor[] all = BeanUtil.getPropertyDescriptors(type); + if (read && write) { + return all; + } else { + List properties = new ArrayList<>(all.length); + for (PropertyDescriptor pd : all) { + if (read && pd.getReadMethod() != null) { + properties.add(pd); + } else if (write && pd.getWriteMethod() != null) { + properties.add(pd); + } + } + return properties.toArray(new PropertyDescriptor[0]); + } + } catch (BeansException ex) { + throw new CodeGenerationException(ex); + } + } + + /** + * 获取 bean 的属性信息 + * @param propertyType 类型 + * @param propertyName 属性名 + * @return {Property} + */ + @Nullable + public static Property getProperty(Class propertyType, String propertyName) { + PropertyDescriptor propertyDescriptor = BeanUtil.getPropertyDescriptor(propertyType, propertyName); + if (propertyDescriptor == null) { + return null; + } + return ReflectUtil.getProperty(propertyType, propertyDescriptor, propertyName); + } + + /** + * 获取 bean 的属性信息 + * @param propertyType 类型 + * @param propertyDescriptor PropertyDescriptor + * @param propertyName 属性名 + * @return {Property} + */ + public static Property getProperty(Class propertyType, PropertyDescriptor propertyDescriptor, String propertyName) { + Method readMethod = propertyDescriptor.getReadMethod(); + Method writeMethod = propertyDescriptor.getWriteMethod(); + return new Property(propertyType, readMethod, writeMethod, propertyName); + } + + /** + * 获取 bean 的属性信息 + * @param propertyType 类型 + * @param propertyName 属性名 + * @return {Property} + */ + @Nullable + public static TypeDescriptor getTypeDescriptor(Class propertyType, String propertyName) { + Property property = ReflectUtil.getProperty(propertyType, propertyName); + if (property == null) { + return null; + } + return new TypeDescriptor(property); + } + + /** + * 获取 类属性信息 + * @param propertyType 类型 + * @param propertyDescriptor PropertyDescriptor + * @param propertyName 属性名 + * @return {Property} + */ + public static TypeDescriptor getTypeDescriptor(Class propertyType, PropertyDescriptor propertyDescriptor, String propertyName) { + Method readMethod = propertyDescriptor.getReadMethod(); + Method writeMethod = propertyDescriptor.getWriteMethod(); + Property property = new Property(propertyType, readMethod, writeMethod, propertyName); + return new TypeDescriptor(property); + } + + /** + * 获取 类属性 + * @param clazz 类信息 + * @param fieldName 属性名 + * @return Field + */ + @Nullable + public static Field getField(Class clazz, String fieldName) { + while (clazz != Object.class) { + try { + return clazz.getDeclaredField(fieldName); + } catch (NoSuchFieldException e) { + clazz = clazz.getSuperclass(); + } + } + return null; + } + + /** + * 获取 所有 field 属性上的注解 + * @param clazz 类 + * @param fieldName 属性名 + * @param annotationClass 注解 + * @param 注解泛型 + * @return 注解 + */ + @Nullable + public static T getAnnotation(Class clazz, String fieldName, Class annotationClass) { + Field field = ReflectUtil.getField(clazz, fieldName); + if (field == null) { + return null; + } + return field.getAnnotation(annotationClass); + } +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/RegexUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/RegexUtil.java new file mode 100644 index 0000000..593cc9f --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/RegexUtil.java @@ -0,0 +1,123 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.tool.utils; + +import org.springframework.lang.Nullable; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * 正则表达式工具 + * + * @author L.cm + */ +public class RegexUtil { + /** + * 用户名 + */ + public static final String USER_NAME = "^[a-zA-Z\\u4E00-\\u9FA5][a-zA-Z0-9_\\u4E00-\\u9FA5]{1,11}$"; + + /** + * 密码 + */ + public static final String USER_PASSWORD = "^.{6,32}$"; + + /** + * 邮箱 + */ + public static final String EMAIL = "^\\w+([-+.]*\\w+)*@([\\da-z](-[\\da-z])?)+(\\.{1,2}[a-z]+)+$"; + + /** + * 手机号 + */ + public static final String PHONE = "^1[3456789]\\d{9}$"; + + /** + * 手机号或者邮箱 + */ + public static final String EMAIL_OR_PHONE = EMAIL + "|" + PHONE; + + /** + * URL路径 + */ + public static final String URL = "^(https?:\\/\\/)?([\\da-z\\.-]+)\\.([a-z\\.]{2,6})(:[\\d]+)?([\\/\\w\\.-]*)*\\/?$"; + + /** + * 身份证校验,初级校验,具体规则有一套算法 + */ + public static final String ID_CARD = "^\\d{15}$|^\\d{17}([0-9]|X)$"; + + /** + * 域名校验 + */ + public static final String DOMAIN = "^[0-9a-zA-Z]+[0-9a-zA-Z\\.-]*\\.[a-zA-Z]{2,4}$"; + + /** + * 编译传入正则表达式和字符串去匹配,忽略大小写 + * + * @param regex 正则 + * @param beTestString 字符串 + * @return {boolean} + */ + public static boolean match(String regex, String beTestString) { + Pattern pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE); + Matcher matcher = pattern.matcher(beTestString); + return matcher.matches(); + } + + /** + * 编译传入正则表达式在字符串中寻找,如果匹配到则为true + * + * @param regex 正则 + * @param beTestString 字符串 + * @return {boolean} + */ + public static boolean find(String regex, String beTestString) { + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(beTestString); + return matcher.find(); + } + + /** + * 编译传入正则表达式在字符串中寻找,如果找到返回第一个结果 + * 找不到返回null + * + * @param regex 正则 + * @param beFoundString 字符串 + * @return {boolean} + */ + @Nullable + public static String findResult(String regex, String beFoundString) { + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(beFoundString); + if (matcher.find()) { + return matcher.group(); + } + return null; + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/ResourceUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/ResourceUtil.java new file mode 100644 index 0000000..84d7c80 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/ResourceUtil.java @@ -0,0 +1,81 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.tool.utils; + +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.UrlResource; +import org.springframework.core.io.support.ResourcePatternResolver; +import org.springframework.util.Assert; + +import java.io.IOException; + +/** + * 资源工具类 + * + * @author L.cm + */ +public class ResourceUtil extends org.springframework.util.ResourceUtils { + public static final String HTTP_REGEX = "^https?:.+$"; + public static final String FTP_URL_PREFIX = "ftp:"; + + /** + * 获取资源 + *

+ * 支持一下协议: + *

+ * 1. classpath: + * 2. file: + * 3. ftp: + * 4. http: and https: + * 5. classpath*: + * 6. C:/dir1/ and /Users/lcm + *

+ * + * @param resourceLocation 资源路径 + * @return {Resource} + * @throws IOException IOException + */ + public static Resource getResource(String resourceLocation) throws IOException { + Assert.notNull(resourceLocation, "Resource location must not be null"); + if (resourceLocation.startsWith(CLASSPATH_URL_PREFIX)) { + String path = resourceLocation.substring(CLASSPATH_URL_PREFIX.length()); + return new ClassPathResource(path); + } + if (resourceLocation.startsWith(FTP_URL_PREFIX)) { + return new UrlResource(resourceLocation); + } + if (resourceLocation.matches(HTTP_REGEX)) { + return new UrlResource(resourceLocation); + } + if (resourceLocation.startsWith(ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX)) { + return SpringUtil.getContext().getResource(resourceLocation); + } + return new FileSystemResource(resourceLocation); + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/RsaUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/RsaUtil.java new file mode 100644 index 0000000..9730f64 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/RsaUtil.java @@ -0,0 +1,390 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.tool.utils; + +import org.springframework.lang.Nullable; +import org.springframework.util.Base64Utils; + +import org.springblade.core.tool.tuple.KeyPair; +import javax.crypto.Cipher; +import java.math.BigInteger; +import java.security.*; +import java.security.spec.*; +import java.util.Objects; + +/** + * RSA加、解密工具 + * + *

+ * 1. 公钥负责加密,私钥负责解密; + * 2. 私钥负责签名,公钥负责验证。 + *

+ * + * @author L.cm + */ +public class RsaUtil { + /** + * 数字签名,密钥算法 + */ + public static final String RSA_ALGORITHM = "RSA"; + public static final String RSA_PADDING = "RSA/ECB/PKCS1Padding"; + + /** + * 获取 KeyPair + * + * @return KeyPair + */ + public static KeyPair genKeyPair() { + return genKeyPair(1024); + } + + /** + * 获取 KeyPair + * + * @param keySize key size + * @return KeyPair + */ + public static KeyPair genKeyPair(int keySize) { + try { + KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance(RSA_ALGORITHM); + // 密钥位数 + keyPairGen.initialize(keySize); + // 密钥对 + return new KeyPair(keyPairGen.generateKeyPair()); + } catch (NoSuchAlgorithmException e) { + throw Exceptions.unchecked(e); + } + } + + /** + * 生成RSA私钥 + * + * @param modulus N特征值 + * @param exponent d特征值 + * @return {@link PrivateKey} + */ + public static PrivateKey generatePrivateKey(String modulus, String exponent) { + return generatePrivateKey(new BigInteger(modulus), new BigInteger(exponent)); + } + + /** + * 生成RSA私钥 + * + * @param modulus N特征值 + * @param exponent d特征值 + * @return {@link PrivateKey} + */ + public static PrivateKey generatePrivateKey(BigInteger modulus, BigInteger exponent) { + RSAPrivateKeySpec keySpec = new RSAPrivateKeySpec(modulus, exponent); + try { + KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM); + return keyFactory.generatePrivate(keySpec); + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { + throw Exceptions.unchecked(e); + } + } + + /** + * 生成RSA公钥 + * + * @param modulus N特征值 + * @param exponent e特征值 + * @return {@link PublicKey} + */ + public static PublicKey generatePublicKey(String modulus, String exponent) { + return generatePublicKey(new BigInteger(modulus), new BigInteger(exponent)); + } + + /** + * 生成RSA公钥 + * + * @param modulus N特征值 + * @param exponent e特征值 + * @return {@link PublicKey} + */ + public static PublicKey generatePublicKey(BigInteger modulus, BigInteger exponent) { + RSAPublicKeySpec keySpec = new RSAPublicKeySpec(modulus, exponent); + try { + KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM); + return keyFactory.generatePublic(keySpec); + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { + throw Exceptions.unchecked(e); + } + } + + /** + * 得到公钥 + * + * @param base64PubKey 密钥字符串(经过base64编码) + * @return PublicKey + */ + public static PublicKey getPublicKey(String base64PubKey) { + Objects.requireNonNull(base64PubKey, "base64 public key is null."); + byte[] keyBytes = Base64Util.decodeFromString(base64PubKey); + X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes); + try { + KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM); + return keyFactory.generatePublic(keySpec); + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { + throw Exceptions.unchecked(e); + } + } + + /** + * 得到公钥字符串 + * + * @param base64PubKey 密钥字符串(经过base64编码) + * @return PublicKey String + */ + public static String getPublicKeyToBase64(String base64PubKey) { + PublicKey publicKey = getPublicKey(base64PubKey); + return getKeyString(publicKey); + } + + /** + * 得到私钥 + * + * @param base64PriKey 密钥字符串(经过base64编码) + * @return PrivateKey + */ + public static PrivateKey getPrivateKey(String base64PriKey) { + Objects.requireNonNull(base64PriKey, "base64 private key is null."); + byte[] keyBytes = Base64Util.decodeFromString(base64PriKey); + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes); + try { + KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM); + return keyFactory.generatePrivate(keySpec); + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { + throw Exceptions.unchecked(e); + } + } + + /** + * 得到密钥字符串(经过base64编码) + * + * @param key key + * @return base 64 编码后的 key + */ + public static String getKeyString(Key key) { + return Base64Util.encodeToString(key.getEncoded()); + } + + /** + * 得到私钥 base64 + * + * @param base64PriKey 密钥字符串(经过base64编码) + * @return PrivateKey String + */ + public static String getPrivateKeyToBase64(String base64PriKey) { + PrivateKey privateKey = getPrivateKey(base64PriKey); + return getKeyString(privateKey); + } + + /** + * 共要加密 + * + * @param base64PublicKey base64 的公钥 + * @param data 待加密的内容 + * @return 加密后的内容 + */ + public static byte[] encrypt(String base64PublicKey, byte[] data) { + return encrypt(getPublicKey(base64PublicKey), data); + } + + /** + * 共要加密 + * + * @param publicKey 公钥 + * @param data 待加密的内容 + * @return 加密后的内容 + */ + public static byte[] encrypt(PublicKey publicKey, byte[] data) { + return rsa(publicKey, data, Cipher.ENCRYPT_MODE); + } + + /** + * 私钥加密,用于 qpp 内,公钥解密 + * + * @param base64PrivateKey base64 的私钥 + * @param data 待加密的内容 + * @return 加密后的内容 + */ + public static byte[] encryptByPrivateKey(String base64PrivateKey, byte[] data) { + return encryptByPrivateKey(getPrivateKey(base64PrivateKey), data); + } + + /** + * 私钥加密,加密成 base64 字符串,用于 qpp 内,公钥解密 + * + * @param base64PrivateKey base64 的私钥 + * @param data 待加密的内容 + * @return 加密后的内容 + */ + public static String encryptByPrivateKeyToBase64(String base64PrivateKey, byte[] data) { + return Base64Util.encodeToString(encryptByPrivateKey(base64PrivateKey, data)); + } + + /** + * 私钥加密,用于 qpp 内,公钥解密 + * + * @param privateKey 私钥 + * @param data 待加密的内容 + * @return 加密后的内容 + */ + public static byte[] encryptByPrivateKey(PrivateKey privateKey, byte[] data) { + return rsa(privateKey, data, Cipher.ENCRYPT_MODE); + } + + /** + * 公钥加密 + * + * @param base64PublicKey base64 公钥 + * @param data 待加密的内容 + * @return 加密后的内容 + */ + @Nullable + public static String encryptToBase64(String base64PublicKey, @Nullable String data) { + if (StringUtil.isBlank(data)) { + return null; + } + return Base64Util.encodeToString(encrypt(base64PublicKey, data.getBytes(Charsets.UTF_8))); + } + + /** + * 解密 + * + * @param base64PrivateKey base64 私钥 + * @param data 数据 + * @return 解密后的数据 + */ + public static byte[] decrypt(String base64PrivateKey, byte[] data) { + return decrypt(getPrivateKey(base64PrivateKey), data); + } + + /** + * 解密 + * + * @param base64publicKey base64 公钥 + * @param data 数据 + * @return 解密后的数据 + */ + public static byte[] decryptByPublicKey(String base64publicKey, byte[] data) { + return decryptByPublicKey(getPublicKey(base64publicKey), data); + } + + /** + * 解密 + * + * @param privateKey privateKey + * @param data 数据 + * @return 解密后的数据 + */ + public static byte[] decrypt(PrivateKey privateKey, byte[] data) { + return rsa(privateKey, data, Cipher.DECRYPT_MODE); + } + + /** + * 解密 + * + * @param publicKey PublicKey + * @param data 数据 + * @return 解密后的数据 + */ + public static byte[] decryptByPublicKey(PublicKey publicKey, byte[] data) { + return rsa(publicKey, data, Cipher.DECRYPT_MODE); + } + + /** + * rsa 加、解密 + * + * @param key key + * @param data 数据 + * @param mode 模式 + * @return 解密后的数据 + */ + private static byte[] rsa(Key key, byte[] data, int mode) { + try { + Cipher cipher = Cipher.getInstance(RSA_PADDING); + cipher.init(mode, key); + return cipher.doFinal(data); + } catch (Exception e) { + throw Exceptions.unchecked(e); + } + } + + /** + * base64 数据解密 + * + * @param base64PublicKey base64 公钥 + * @param base64Data base64数据 + * @return 解密后的数据 + */ + public static byte[] decryptByPublicKeyFromBase64(String base64PublicKey, byte[] base64Data) { + return decryptByPublicKey(getPublicKey(base64PublicKey), base64Data); + } + + /** + * base64 数据解密 + * + * @param base64PrivateKey base64 私钥 + * @param base64Data base64数据 + * @return 解密后的数据 + */ + @Nullable + public static String decryptFromBase64(String base64PrivateKey, @Nullable String base64Data) { + if (StringUtil.isBlank(base64Data)) { + return null; + } + return new String(decrypt(base64PrivateKey, Base64Util.decodeFromString(base64Data)), Charsets.UTF_8); + } + + /** + * base64 数据解密 + * + * @param base64PrivateKey base64 私钥 + * @param base64Data base64数据 + * @return 解密后的数据 + */ + public static byte[] decryptFromBase64(String base64PrivateKey, byte[] base64Data) { + return decrypt(base64PrivateKey, Base64Util.decode(base64Data)); + } + + /** + * base64 数据解密 + * + * @param base64PublicKey base64 公钥 + * @param base64Data base64数据 + * @return 解密后的数据 + */ + @Nullable + public static String decryptByPublicKeyFromBase64(String base64PublicKey, @Nullable String base64Data) { + if (StringUtil.isBlank(base64Data)) { + return null; + } + return new String(decryptByPublicKeyFromBase64(base64PublicKey, Base64Util.decodeFromString(base64Data)), Charsets.UTF_8); + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/RuntimeUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/RuntimeUtil.java new file mode 100644 index 0000000..c88919f --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/RuntimeUtil.java @@ -0,0 +1,86 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.tool.utils; + + +import java.lang.management.ManagementFactory; +import java.time.Duration; +import java.util.List; + +/** + * 运行时工具类 + * + * @author L.cm + */ +public class RuntimeUtil { + + /** + * 获得当前进程的PID + *

+ * 当失败时返回-1 + * + * @return pid + */ + public static int getPid() { + // something like '@', at least in SUN / Oracle JVMs + final String jvmName = ManagementFactory.getRuntimeMXBean().getName(); + final int index = jvmName.indexOf(CharPool.AT); + if (index > 0) { + return NumberUtil.toInt(jvmName.substring(0, index), -1); + } + return -1; + } + + /** + * 返回应用启动到现在的时间 + * + * @return {Duration} + */ + public static Duration getUpTime() { + long upTime = ManagementFactory.getRuntimeMXBean().getUptime(); + return Duration.ofMillis(upTime); + } + + /** + * 返回输入的JVM参数列表 + * + * @return jvm参数 + */ + public static String getJvmArguments() { + List vmArguments = ManagementFactory.getRuntimeMXBean().getInputArguments(); + return StringUtil.join(vmArguments, StringPool.SPACE); + } + + /** + * 获取CPU核数 + * + * @return cpu count + */ + public static int getCpuNum() { + return Runtime.getRuntime().availableProcessors(); + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/SpringUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/SpringUtil.java new file mode 100644 index 0000000..7cbd769 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/SpringUtil.java @@ -0,0 +1,124 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.tool.utils; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ApplicationEvent; +import org.springframework.lang.Nullable; + +/** + * spring 工具类 + * + * @author Chill + */ +@Slf4j +public class SpringUtil implements ApplicationContextAware { + + private static ApplicationContext context; + + @Override + public void setApplicationContext(@Nullable ApplicationContext context) throws BeansException { + SpringUtil.context = context; + } + + /** + * 获取bean + * + * @param clazz class类 + * @param 泛型 + * @return T + */ + public static T getBean(Class clazz) { + if (clazz == null) { + return null; + } + return context.getBean(clazz); + } + + /** + * 获取bean + * + * @param beanId beanId + * @param 泛型 + * @return T + */ + public static T getBean(String beanId) { + if (beanId == null) { + return null; + } + return (T) context.getBean(beanId); + } + + /** + * 获取bean + * + * @param beanName bean名称 + * @param clazz class类 + * @param 泛型 + * @return T + */ + public static T getBean(String beanName, Class clazz) { + if (null == beanName || "".equals(beanName.trim())) { + return null; + } + if (clazz == null) { + return null; + } + return (T) context.getBean(beanName, clazz); + } + + /** + * 获取 ApplicationContext + * + * @return ApplicationContext + */ + public static ApplicationContext getContext() { + if (context == null) { + return null; + } + return context; + } + + /** + * 发布事件 + * + * @param event 事件 + */ + public static void publishEvent(ApplicationEvent event) { + if (context == null) { + return; + } + try { + context.publishEvent(event); + } catch (Exception ex) { + log.error(ex.getMessage()); + } + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/StreamUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/StreamUtil.java new file mode 100644 index 0000000..d97943d --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/StreamUtil.java @@ -0,0 +1,754 @@ +/* + * Copyright (C) 2015 The Guava Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you + * may not use this file except in compliance with the License. You may + * obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.springblade.core.tool.utils; + +import java.util.*; +import java.util.Spliterators.AbstractSpliterator; +import java.util.function.*; +import java.util.stream.*; + +import static java.lang.Math.min; +import static java.util.Objects.requireNonNull; + +/** + * Static utility methods related to {@code Stream} instances. + * + * @author Guava + */ +public class StreamUtil { + + /** + * Returns a sequential {@link Stream} of the contents of {@code iterable}, delegating to {@link + * Collection#stream} if possible. + */ + public static Stream stream(Iterable iterable) { + return (iterable instanceof Collection) + ? ((Collection) iterable).stream() + : StreamSupport.stream(iterable.spliterator(), false); + } + + /** + * Returns a sequential {@link Stream} of the remaining contents of {@code iterator}. Do not use + * {@code iterator} directly after passing it to this method. + */ + public static Stream stream(Iterator iterator) { + return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, 0), false); + } + + /** + * If a value is present in {@code optional}, returns a stream containing only that element, + * otherwise returns an empty stream. + */ + public static Stream stream(Optional optional) { + return optional.map(Stream::of).orElseGet(Stream::empty); + } + + /** + * If a value is present in {@code optional}, returns a stream containing only that element, + * otherwise returns an empty stream. + * + *

Java 9 users: use {@code optional.stream()} instead. + */ + public static IntStream stream(OptionalInt optional) { + return optional.isPresent() ? IntStream.of(optional.getAsInt()) : IntStream.empty(); + } + + /** + * If a value is present in {@code optional}, returns a stream containing only that element, + * otherwise returns an empty stream. + * + *

Java 9 users: use {@code optional.stream()} instead. + */ + public static LongStream stream(OptionalLong optional) { + return optional.isPresent() ? LongStream.of(optional.getAsLong()) : LongStream.empty(); + } + + /** + * If a value is present in {@code optional}, returns a stream containing only that element, + * otherwise returns an empty stream. + * + *

Java 9 users: use {@code optional.stream()} instead. + */ + public static DoubleStream stream(OptionalDouble optional) { + return optional.isPresent() ? DoubleStream.of(optional.getAsDouble()) : DoubleStream.empty(); + } + + /** + * Returns a stream in which each element is the result of passing the corresponding element of + * each of {@code streamA} and {@code streamB} to {@code function}. + * + *

For example: + * + *

{@code
+	 * Streams.zip(
+	 *   Stream.of("foo1", "foo2", "foo3"),
+	 *   Stream.of("bar1", "bar2"),
+	 *   (arg1, arg2) -> arg1 + ":" + arg2)
+	 * }
+ * + *

will return {@code Stream.of("foo1:bar1", "foo2:bar2")}. + * + *

The resulting stream will only be as long as the shorter of the two input streams; if one + * stream is longer, its extra elements will be ignored. + * + *

Note that if you are calling {@link Stream#forEach} on the resulting stream, you might want + * to consider using {@link #forEachPair} instead of this method. + * + *

Performance note: The resulting stream is not efficiently splittable. + * This may harm parallel performance. + */ + public static Stream zip(Stream streamA, Stream streamB, BiFunction function) { + // same as Stream.concat + boolean isParallel = streamA.isParallel() || streamB.isParallel(); + Spliterator splitA = streamA.spliterator(); + Spliterator splitB = streamB.spliterator(); + int characteristics = + splitA.characteristics() + & splitB.characteristics() + & (Spliterator.SIZED | Spliterator.ORDERED); + Iterator itrA = Spliterators.iterator(splitA); + Iterator itrB = Spliterators.iterator(splitB); + return StreamSupport.stream( + new AbstractSpliterator( + min(splitA.estimateSize(), splitB.estimateSize()), characteristics) { + @Override + public boolean tryAdvance(Consumer action) { + if (itrA.hasNext() && itrB.hasNext()) { + action.accept(function.apply(itrA.next(), itrB.next())); + return true; + } + return false; + } + }, + isParallel) + .onClose(streamA::close) + .onClose(streamB::close); + } + + /** + * Invokes {@code consumer} once for each pair of corresponding elements in {@code streamA} + * and {@code streamB}. If one stream is longer than the other, the extra elements are silently + * ignored. Elements passed to the consumer are guaranteed to come from the same position in their + * respective source streams. For example: + * + *

{@code
+	 * Streams.forEachPair(
+	 *   Stream.of("foo1", "foo2", "foo3"),
+	 *   Stream.of("bar1", "bar2"),
+	 *   (arg1, arg2) -> System.out.println(arg1 + ":" + arg2)
+	 * }
+ * + *

will print: + * + *

{@code
+	 * foo1:bar1
+	 * foo2:bar2
+	 * }
+ * + *

Warning: If either supplied stream is a parallel stream, the same correspondence + * between elements will be made, but the order in which those pairs of elements are passed to the + * consumer is not defined. + * + *

Note that many usages of this method can be replaced with simpler calls to {@link #zip}. + * This method behaves equivalently to {@linkplain #zip zipping} the stream elements into + * temporary pair objects and then using {@link Stream#forEach} on that stream. + */ + public static void forEachPair(Stream streamA, Stream streamB, BiConsumer consumer) { + if (streamA.isParallel() || streamB.isParallel()) { + zip(streamA, streamB, TemporaryPair::new).forEach(pair -> consumer.accept(pair.a, pair.b)); + } else { + Iterator iterA = streamA.iterator(); + Iterator iterB = streamB.iterator(); + while (iterA.hasNext() && iterB.hasNext()) { + consumer.accept(iterA.next(), iterB.next()); + } + } + } + + // Use this carefully - it doesn't implement value semantics + private static class TemporaryPair { + final A a; + final B b; + + TemporaryPair(A a, B b) { + this.a = a; + this.b = b; + } + } + + /** + * Returns a stream consisting of the results of applying the given function to the elements of + * {@code stream} and their indices in the stream. For example, + * + *

{@code
+	 * mapWithIndex(
+	 *     Stream.of("a", "b", "c"),
+	 *     (e, index) -> index + ":" + e)
+	 * }
+ * + *

would return {@code Stream.of("0:a", "1:b", "2:c")}. + * + *

The resulting stream is efficiently splittable + * if and only if {@code stream} was efficiently splittable and its underlying spliterator + * reported {@link Spliterator#SUBSIZED}. This is generally the case if the underlying stream + * comes from a data structure supporting efficient indexed random access, typically an array or + * list. + * + *

The order of the resulting stream is defined if and only if the order of the original stream + * was defined. + */ + public static Stream mapWithIndex(Stream stream, FunctionWithIndex function) { + boolean isParallel = stream.isParallel(); + Spliterator fromSpliterator = stream.spliterator(); + + if (!fromSpliterator.hasCharacteristics(Spliterator.SUBSIZED)) { + Iterator fromIterator = Spliterators.iterator(fromSpliterator); + return StreamSupport.stream( + new AbstractSpliterator( + fromSpliterator.estimateSize(), + fromSpliterator.characteristics() & (Spliterator.ORDERED | Spliterator.SIZED)) { + long index = 0; + + @Override + public boolean tryAdvance(Consumer action) { + if (fromIterator.hasNext()) { + action.accept(function.apply(fromIterator.next(), index++)); + return true; + } + return false; + } + }, + isParallel) + .onClose(stream::close); + } + class Splitr extends MapWithIndexSpliterator, R, Splitr> implements Consumer { + T holder; + + Splitr(Spliterator splitr, long index) { + super(splitr, index); + } + + @Override + public void accept(T t) { + this.holder = t; + } + + @Override + public boolean tryAdvance(Consumer action) { + if (fromSpliterator.tryAdvance(this)) { + try { + // The cast is safe because tryAdvance puts a T into `holder`. + action.accept(function.apply(uncheckedCastNullableTToT(holder), index++)); + return true; + } finally { + holder = null; + } + } + return false; + } + + @Override + Splitr createSplit(Spliterator from, long i) { + return new Splitr(from, i); + } + } + return StreamSupport.stream(new Splitr(fromSpliterator, 0), isParallel).onClose(stream::close); + } + + /** + * Returns a stream consisting of the results of applying the given function to the elements of + * {@code stream} and their indexes in the stream. For example, + * + *

{@code
+	 * mapWithIndex(
+	 *     IntStream.of(10, 11, 12),
+	 *     (e, index) -> index + ":" + e)
+	 * }
+ * + *

...would return {@code Stream.of("0:10", "1:11", "2:12")}. + * + *

The resulting stream is efficiently splittable + * if and only if {@code stream} was efficiently splittable and its underlying spliterator + * reported {@link Spliterator#SUBSIZED}. This is generally the case if the underlying stream + * comes from a data structure supporting efficient indexed random access, typically an array or + * list. + * + *

The order of the resulting stream is defined if and only if the order of the original stream + * was defined. + */ + public static Stream mapWithIndex(IntStream stream, IntFunctionWithIndex function) { + boolean isParallel = stream.isParallel(); + Spliterator.OfInt fromSpliterator = stream.spliterator(); + if (!fromSpliterator.hasCharacteristics(Spliterator.SUBSIZED)) { + PrimitiveIterator.OfInt fromIterator = Spliterators.iterator(fromSpliterator); + return StreamSupport.stream( + new AbstractSpliterator( + fromSpliterator.estimateSize(), + fromSpliterator.characteristics() & (Spliterator.ORDERED | Spliterator.SIZED)) { + long index = 0; + + @Override + public boolean tryAdvance(Consumer action) { + if (fromIterator.hasNext()) { + action.accept(function.apply(fromIterator.nextInt(), index++)); + return true; + } + return false; + } + }, + isParallel) + .onClose(stream::close); + } + class Splitr extends MapWithIndexSpliterator + implements IntConsumer, Spliterator { + int holder; + + Splitr(OfInt splitr, long index) { + super(splitr, index); + } + + @Override + public void accept(int t) { + this.holder = t; + } + + @Override + public boolean tryAdvance(Consumer action) { + if (fromSpliterator.tryAdvance(this)) { + action.accept(function.apply(holder, index++)); + return true; + } + return false; + } + + @Override + Splitr createSplit(OfInt from, long i) { + return new Splitr(from, i); + } + } + return StreamSupport.stream(new Splitr(fromSpliterator, 0), isParallel).onClose(stream::close); + } + + /** + * Returns a stream consisting of the results of applying the given function to the elements of + * {@code stream} and their indexes in the stream. For example, + * + *

{@code
+	 * mapWithIndex(
+	 *     LongStream.of(10, 11, 12),
+	 *     (e, index) -> index + ":" + e)
+	 * }
+ * + *

...would return {@code Stream.of("0:10", "1:11", "2:12")}. + * + *

The resulting stream is efficiently splittable + * if and only if {@code stream} was efficiently splittable and its underlying spliterator + * reported {@link Spliterator#SUBSIZED}. This is generally the case if the underlying stream + * comes from a data structure supporting efficient indexed random access, typically an array or + * list. + * + *

The order of the resulting stream is defined if and only if the order of the original stream + * was defined. + */ + public static Stream mapWithIndex(LongStream stream, LongFunctionWithIndex function) { + boolean isParallel = stream.isParallel(); + Spliterator.OfLong fromSpliterator = stream.spliterator(); + + if (!fromSpliterator.hasCharacteristics(Spliterator.SUBSIZED)) { + PrimitiveIterator.OfLong fromIterator = Spliterators.iterator(fromSpliterator); + return StreamSupport.stream( + new AbstractSpliterator( + fromSpliterator.estimateSize(), + fromSpliterator.characteristics() & (Spliterator.ORDERED | Spliterator.SIZED)) { + long index = 0; + + @Override + public boolean tryAdvance(Consumer action) { + if (fromIterator.hasNext()) { + action.accept(function.apply(fromIterator.nextLong(), index++)); + return true; + } + return false; + } + }, + isParallel) + .onClose(stream::close); + } + class Splitr extends MapWithIndexSpliterator + implements LongConsumer, Spliterator { + long holder; + + Splitr(OfLong splitr, long index) { + super(splitr, index); + } + + @Override + public void accept(long t) { + this.holder = t; + } + + @Override + public boolean tryAdvance(Consumer action) { + if (fromSpliterator.tryAdvance(this)) { + action.accept(function.apply(holder, index++)); + return true; + } + return false; + } + + @Override + Splitr createSplit(OfLong from, long i) { + return new Splitr(from, i); + } + } + return StreamSupport.stream(new Splitr(fromSpliterator, 0), isParallel).onClose(stream::close); + } + + /** + * Returns a stream consisting of the results of applying the given function to the elements of + * {@code stream} and their indexes in the stream. For example, + * + *

{@code
+	 * mapWithIndex(
+	 *     DoubleStream.of(0.0, 1.0, 2.0)
+	 *     (e, index) -> index + ":" + e)
+	 * }
+ * + *

...would return {@code Stream.of("0:0.0", "1:1.0", "2:2.0")}. + * + *

The resulting stream is efficiently splittable + * if and only if {@code stream} was efficiently splittable and its underlying spliterator + * reported {@link Spliterator#SUBSIZED}. This is generally the case if the underlying stream + * comes from a data structure supporting efficient indexed random access, typically an array or + * list. + * + *

The order of the resulting stream is defined if and only if the order of the original stream + * was defined. + */ + public static Stream mapWithIndex(DoubleStream stream, DoubleFunctionWithIndex function) { + boolean isParallel = stream.isParallel(); + Spliterator.OfDouble fromSpliterator = stream.spliterator(); + + if (!fromSpliterator.hasCharacteristics(Spliterator.SUBSIZED)) { + PrimitiveIterator.OfDouble fromIterator = Spliterators.iterator(fromSpliterator); + return StreamSupport.stream( + new AbstractSpliterator( + fromSpliterator.estimateSize(), + fromSpliterator.characteristics() & (Spliterator.ORDERED | Spliterator.SIZED)) { + long index = 0; + + @Override + public boolean tryAdvance(Consumer action) { + if (fromIterator.hasNext()) { + action.accept(function.apply(fromIterator.nextDouble(), index++)); + return true; + } + return false; + } + }, + isParallel) + .onClose(stream::close); + } + class Splitr extends MapWithIndexSpliterator + implements DoubleConsumer, Spliterator { + double holder; + + Splitr(OfDouble splitr, long index) { + super(splitr, index); + } + + @Override + public void accept(double t) { + this.holder = t; + } + + @Override + public boolean tryAdvance(Consumer action) { + if (fromSpliterator.tryAdvance(this)) { + action.accept(function.apply(holder, index++)); + return true; + } + return false; + } + + @Override + Splitr createSplit(OfDouble from, long i) { + return new Splitr(from, i); + } + } + return StreamSupport.stream(new Splitr(fromSpliterator, 0), isParallel).onClose(stream::close); + } + + /** + * An analogue of {@link Function} also accepting an index. + * + *

This interface is only intended for use by callers of {@link #mapWithIndex(Stream, + * FunctionWithIndex)}. + * + * @since 21.0 + */ + public interface FunctionWithIndex { + /** + * Applies this function to the given argument and its index within a stream. + */ + + R apply(T from, long index); + } + + private abstract static class MapWithIndexSpliterator< + F extends Spliterator, + R, + S extends MapWithIndexSpliterator> + implements Spliterator { + final F fromSpliterator; + long index; + + MapWithIndexSpliterator(F fromSpliterator, long index) { + this.fromSpliterator = fromSpliterator; + this.index = index; + } + + abstract S createSplit(F from, long i); + + @Override + public S trySplit() { + Spliterator splitOrNull = fromSpliterator.trySplit(); + if (splitOrNull == null) { + return null; + } + @SuppressWarnings("unchecked") + F split = (F) splitOrNull; + S result = createSplit(split, index); + this.index += split.getExactSizeIfKnown(); + return result; + } + + @Override + public long estimateSize() { + return fromSpliterator.estimateSize(); + } + + @Override + public int characteristics() { + return fromSpliterator.characteristics() + & (Spliterator.ORDERED | Spliterator.SIZED | Spliterator.SUBSIZED); + } + } + + /** + * An analogue of {@link IntFunction} also accepting an index. + * + *

This interface is only intended for use by callers of {@link #mapWithIndex(IntStream, + * IntFunctionWithIndex)}. + * + * @since 21.0 + */ + public interface IntFunctionWithIndex { + /** + * Applies this function to the given argument and its index within a stream. + */ + + R apply(int from, long index); + } + + /** + * An analogue of {@link LongFunction} also accepting an index. + * + *

This interface is only intended for use by callers of {@link #mapWithIndex(LongStream, + * LongFunctionWithIndex)}. + * + * @since 21.0 + */ + public interface LongFunctionWithIndex { + /** + * Applies this function to the given argument and its index within a stream. + */ + + R apply(long from, long index); + } + + /** + * An analogue of {@link DoubleFunction} also accepting an index. + * + *

This interface is only intended for use by callers of {@link #mapWithIndex(DoubleStream, + * DoubleFunctionWithIndex)}. + * + * @since 21.0 + */ + public interface DoubleFunctionWithIndex { + /** + * Applies this function to the given argument and its index within a stream. + */ + + R apply(double from, long index); + } + + /** + * Returns the last element of the specified stream, or {@link Optional#empty} if the + * stream is empty. + * + *

Equivalent to {@code stream.reduce((a, b) -> b)}, but may perform significantly better. This + * method's runtime will be between O(log n) and O(n), performing better on efficiently splittable + * streams. + * + *

If the stream has nondeterministic order, this has equivalent semantics to {@link + * Stream#findAny} (which you might as well use). + * + * @throws NullPointerException if the last element of the stream is null + * @see Stream#findFirst() + */ + /* + * By declaring instead of , we declare this method as requiring a + * stream whose elements are non-null. However, the method goes out of its way to still handle + * nulls in the stream. This means that the method can safely be used with a stream that contains + * nulls as long as the *last* element is *not* null. + * + * (To "go out of its way," the method tracks a `set` bit so that it can distinguish "the final + * split has a last element of null, so throw NPE" from "the final split was empty, so look for an + * element in the prior one.") + */ + public static Optional findLast(Stream stream) { + class OptionalState { + boolean set = false; + T value = null; + + void set(T value) { + this.set = true; + this.value = value; + } + + T get() { + /* + * requireNonNull is safe because we call get() only if we've previously called set(). + * + * (For further discussion of nullness, see the comment above the method.) + */ + return requireNonNull(value); + } + } + OptionalState state = new OptionalState(); + + Deque> splits = new ArrayDeque<>(); + splits.addLast(stream.spliterator()); + + while (!splits.isEmpty()) { + Spliterator spliterator = splits.removeLast(); + + if (spliterator.getExactSizeIfKnown() == 0) { + continue; // drop this split + } + + // Many spliterators will have trySplits that are SUBSIZED even if they are not themselves + // SUBSIZED. + if (spliterator.hasCharacteristics(Spliterator.SUBSIZED)) { + // we can drill down to exactly the smallest nonempty spliterator + while (true) { + Spliterator prefix = spliterator.trySplit(); + if (prefix == null || prefix.getExactSizeIfKnown() == 0) { + break; + } else if (spliterator.getExactSizeIfKnown() == 0) { + spliterator = prefix; + break; + } + } + + // spliterator is known to be nonempty now + spliterator.forEachRemaining(state::set); + return Optional.of(state.get()); + } + + Spliterator prefix = spliterator.trySplit(); + if (prefix == null || prefix.getExactSizeIfKnown() == 0) { + // we can't split this any further + spliterator.forEachRemaining(state::set); + if (state.set) { + return Optional.of(state.get()); + } + // fall back to the last split + continue; + } + splits.addLast(prefix); + splits.addLast(spliterator); + } + return Optional.empty(); + } + + /** + * Returns the last element of the specified stream, or {@link OptionalInt#empty} if the stream is + * empty. + * + *

Equivalent to {@code stream.reduce((a, b) -> b)}, but may perform significantly better. This + * method's runtime will be between O(log n) and O(n), performing better on efficiently splittable + * streams. + * + * @throws NullPointerException if the last element of the stream is null + * @see IntStream#findFirst() + */ + public static OptionalInt findLast(IntStream stream) { + // findLast(Stream) does some allocation, so we might as well box some more + Optional boxedLast = findLast(stream.boxed()); + return boxedLast.map(OptionalInt::of).orElseGet(OptionalInt::empty); + } + + /** + * Returns the last element of the specified stream, or {@link OptionalLong#empty} if the stream + * is empty. + * + *

Equivalent to {@code stream.reduce((a, b) -> b)}, but may perform significantly better. This + * method's runtime will be between O(log n) and O(n), performing better on efficiently splittable + * streams. + * + * @throws NullPointerException if the last element of the stream is null + * @see LongStream#findFirst() + */ + public static OptionalLong findLast(LongStream stream) { + // findLast(Stream) does some allocation, so we might as well box some more + Optional boxedLast = findLast(stream.boxed()); + return boxedLast.map(OptionalLong::of).orElseGet(OptionalLong::empty); + } + + /** + * Returns the last element of the specified stream, or {@link OptionalDouble#empty} if the stream + * is empty. + * + *

Equivalent to {@code stream.reduce((a, b) -> b)}, but may perform significantly better. This + * method's runtime will be between O(log n) and O(n), performing better on efficiently splittable + * streams. + * + * @throws NullPointerException if the last element of the stream is null + * @see DoubleStream#findFirst() + */ + public static OptionalDouble findLast(DoubleStream stream) { + // findLast(Stream) does some allocation, so we might as well box some more + Optional boxedLast = findLast(stream.boxed()); + return boxedLast.map(OptionalDouble::of).orElseGet(OptionalDouble::empty); + } + + public static T uncheckedCastNullableTToT(T t) { + return t; + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/StringPool.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/StringPool.java new file mode 100644 index 0000000..9614f1c --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/StringPool.java @@ -0,0 +1,96 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.tool.utils; + +/** + * 静态 String 池 + * + * @author L.cm + */ +public interface StringPool { + + String AMPERSAND = "&"; + String AND = "and"; + String AT = "@"; + String ASTERISK = "*"; + String STAR = ASTERISK; + String SLASH = "/"; + String BACK_SLASH = "\\"; + String DOUBLE_SLASH = "#//"; + String COLON = ":"; + String COMMA = ","; + String DASH = "-"; + String DOLLAR = "$"; + String DOT = "."; + String EMPTY = ""; + String EMPTY_JSON = "{}"; + String EQUALS = "="; + String FALSE = "false"; + String HASH = "#"; + String HAT = "^"; + String LEFT_BRACE = "{"; + String LEFT_BRACKET = "("; + String LEFT_CHEV = "<"; + String NEWLINE = "\n"; + String N = "n"; + String NO = "no"; + String NULL = "null"; + String OFF = "off"; + String ON = "on"; + String PERCENT = "%"; + String PIPE = "|"; + String PLUS = "+"; + String QUESTION_MARK = "?"; + String EXCLAMATION_MARK = "!"; + String QUOTE = "\""; + String RETURN = "\r"; + String TAB = "\t"; + String RIGHT_BRACE = "}"; + String RIGHT_BRACKET = ")"; + String RIGHT_CHEV = ">"; + String SEMICOLON = ";"; + String SINGLE_QUOTE = "'"; + String BACKTICK = "`"; + String SPACE = " "; + String TILDA = "~"; + String LEFT_SQ_BRACKET = "["; + String RIGHT_SQ_BRACKET = "]"; + String TRUE = "true"; + String UNDERSCORE = "_"; + String UTF_8 = "UTF-8"; + String GBK = "GBK"; + String ISO_8859_1 = "ISO-8859-1"; + String Y = "y"; + String YES = "yes"; + String ONE = "1"; + String ZERO = "0"; + String MINUS_ONE = "-1"; + String DOLLAR_LEFT_BRACE= "${"; + String UNKNOWN = "unknown"; + String GET = "GET"; + String POST = "POST"; + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/StringUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/StringUtil.java new file mode 100644 index 0000000..2a3b13f --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/StringUtil.java @@ -0,0 +1,1586 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.tool.utils; + +import org.springblade.core.tool.support.StrSpliter; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.PatternMatchUtils; +import org.springframework.web.util.HtmlUtils; + +import java.io.StringReader; +import java.io.StringWriter; +import java.text.MessageFormat; +import java.util.*; +import java.util.concurrent.ThreadLocalRandom; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +/** + * 继承自Spring util的工具类,减少jar依赖 + * + * @author L.cm + */ +public class StringUtil extends org.springframework.util.StringUtils { + + public static final int INDEX_NOT_FOUND = -1; + + /** + * Check whether the given {@code CharSequence} contains actual text. + *

More specifically, this method returns {@code true} if the + * {@code CharSequence} is not {@code null}, its length is greater than + * 0, and it contains at least one non-whitespace character. + *

+	 * StringUtil.isBlank(null) = true
+	 * StringUtil.isBlank("") = true
+	 * StringUtil.isBlank(" ") = true
+	 * StringUtil.isBlank("12345") = false
+	 * StringUtil.isBlank(" 12345 ") = false
+	 * 
+ * + * @param cs the {@code CharSequence} to check (may be {@code null}) + * @return {@code true} if the {@code CharSequence} is not {@code null}, + * its length is greater than 0, and it does not contain whitespace only + * @see Character#isWhitespace + */ + public static boolean isBlank(final CharSequence cs) { + return !StringUtil.hasText(cs); + } + + /** + *

Checks if a CharSequence is not empty (""), not null and not whitespace only.

+ *
+	 * StringUtil.isNotBlank(null)	  = false
+	 * StringUtil.isNotBlank("")		= false
+	 * StringUtil.isNotBlank(" ")	   = false
+	 * StringUtil.isNotBlank("bob")	 = true
+	 * StringUtil.isNotBlank("  bob  ") = true
+	 * 
+ * + * @param cs the CharSequence to check, may be null + * @return {@code true} if the CharSequence is + * not empty and not null and not whitespace + * @see Character#isWhitespace + */ + public static boolean isNotBlank(final CharSequence cs) { + return StringUtil.hasText(cs); + } + + /** + * 有 任意 一个 Blank + * + * @param css CharSequence + * @return boolean + */ + public static boolean isAnyBlank(final CharSequence... css) { + if (ObjectUtil.isEmpty(css)) { + return true; + } + return Stream.of(css).anyMatch(StringUtil::isBlank); + } + + /** + * 是否全非 Blank + * + * @param css CharSequence + * @return boolean + */ + public static boolean isNoneBlank(final CharSequence... css) { + if (ObjectUtil.isEmpty(css)) { + return false; + } + return Stream.of(css).allMatch(StringUtil::isNotBlank); + } + + /** + * 是否全为 Blank + * + * @param css CharSequence + * @return boolean + */ + public static boolean isAllBlank(final CharSequence... css) { + return Stream.of(css).allMatch(StringUtil::isBlank); + } + + /** + * 判断一个字符串是否是数字 + * + * @param cs the CharSequence to check, may be null + * @return {boolean} + */ + public static boolean isNumeric(final CharSequence cs) { + if (isBlank(cs)) { + return false; + } + for (int i = cs.length(); --i >= 0; ) { + int chr = cs.charAt(i); + if (chr < 48 || chr > 57) { + return false; + } + } + return true; + } + + /** + * 将字符串中特定模式的字符转换成map中对应的值 + *

+ * use: format("my name is ${name}, and i like ${like}!", {"name":"L.cm", "like": "Java"}) + * + * @param message 需要转换的字符串 + * @param params 转换所需的键值对集合 + * @return 转换后的字符串 + */ + public static String format(@Nullable String message, @Nullable Map params) { + // message 为 null 返回空字符串 + if (message == null) { + return StringPool.EMPTY; + } + // 参数为 null 或者为空 + if (params == null || params.isEmpty()) { + return message; + } + // 替换变量 + StringBuilder sb = new StringBuilder((int) (message.length() * 1.5)); + int cursor = 0; + for (int start, end; (start = message.indexOf(StringPool.DOLLAR_LEFT_BRACE, cursor)) != -1 && (end = message.indexOf(StringPool.RIGHT_BRACE, start)) != -1; ) { + sb.append(message, cursor, start); + String key = message.substring(start + 2, end); + Object value = params.get(key.strip()); + sb.append(value == null ? StringPool.EMPTY : value); + cursor = end + 1; + } + sb.append(message.substring(cursor)); + return sb.toString(); + } + + /** + * 同 log 格式的 format 规则 + *

+ * use: format("my name is {}, and i like {}!", "L.cm", "Java") + * + * @param message 需要转换的字符串 + * @param arguments 需要替换的变量 + * @return 转换后的字符串 + */ + public static String format(@Nullable String message, @Nullable Object... arguments) { + // message 为 null 返回空字符串 + if (message == null) { + return StringPool.EMPTY; + } + // 参数为 null 或者为空 + if (arguments == null || arguments.length == 0) { + return message; + } + StringBuilder sb = new StringBuilder((int) (message.length() * 1.5)); + int cursor = 0; + int index = 0; + int argsLength = arguments.length; + for (int start, end; (start = message.indexOf('{', cursor)) != -1 && (end = message.indexOf('}', start)) != -1 && index < argsLength; ) { + sb.append(message, cursor, start); + sb.append(arguments[index]); + cursor = end + 1; + index++; + } + sb.append(message.substring(cursor)); + return sb.toString(); + } + + /** + * 格式化执行时间,单位为 ms 和 s,保留三位小数 + * + * @param nanos 纳秒 + * @return 格式化后的时间 + */ + public static String format(long nanos) { + if (nanos < 1) { + return "0ms"; + } + double millis = (double) nanos / (1000 * 1000); + // 不够 1 ms,最小单位为 ms + if (millis > 1000) { + return String.format("%.3fs", millis / 1000); + } else { + return String.format("%.3fms", millis); + } + } + + /** + * Convert a {@code Collection} into a delimited {@code String} (e.g., CSV). + *

Useful for {@code toString()} implementations. + * + * @param coll the {@code Collection} to convert + * @return the delimited {@code String} + */ + public static String join(Collection coll) { + return StringUtil.collectionToCommaDelimitedString(coll); + } + + /** + * Convert a {@code Collection} into a delimited {@code String} (e.g. CSV). + *

Useful for {@code toString()} implementations. + * + * @param coll the {@code Collection} to convert + * @param delim the delimiter to use (typically a ",") + * @return the delimited {@code String} + */ + public static String join(Collection coll, String delim) { + return StringUtil.collectionToDelimitedString(coll, delim); + } + + /** + * Convert a {@code String} array into a comma delimited {@code String} + * (i.e., CSV). + *

Useful for {@code toString()} implementations. + * + * @param arr the array to display + * @return the delimited {@code String} + */ + public static String join(Object[] arr) { + return StringUtil.arrayToCommaDelimitedString(arr); + } + + /** + * Convert a {@code String} array into a delimited {@code String} (e.g. CSV). + *

Useful for {@code toString()} implementations. + * + * @param arr the array to display + * @param delim the delimiter to use (typically a ",") + * @return the delimited {@code String} + */ + public static String join(Object[] arr, String delim) { + return StringUtil.arrayToDelimitedString(arr, delim); + } + + /** + * 字符串是否符合指定的 表达式 + * + *

+ * pattern styles: "xxx*", "*xxx", "*xxx*" and "xxx*yyy" + *

+ * + * @param pattern 表达式 + * @param str 字符串 + * @return 是否匹配 + */ + public static boolean simpleMatch(@Nullable String pattern, @Nullable String str) { + return PatternMatchUtils.simpleMatch(pattern, str); + } + + /** + * 字符串是否符合指定的 表达式 + * + *

+ * pattern styles: "xxx*", "*xxx", "*xxx*" and "xxx*yyy" + *

+ * + * @param patterns 表达式 数组 + * @param str 字符串 + * @return 是否匹配 + */ + public static boolean simpleMatch(@Nullable String[] patterns, String str) { + return PatternMatchUtils.simpleMatch(patterns, str); + } + + /** + * 生成uuid + * + * @return UUID + */ + public static String randomUUID() { + ThreadLocalRandom random = ThreadLocalRandom.current(); + return new UUID(random.nextLong(), random.nextLong()).toString().replace(StringPool.DASH, StringPool.EMPTY); + } + + /** + * 转义HTML用于安全过滤 + * + * @param html html + * @return {String} + */ + public static String escapeHtml(String html) { + return StringUtil.isBlank(html) ? StringPool.EMPTY : HtmlUtils.htmlEscape(html); + } + + /** + * 清理字符串,清理出某些不可见字符 + * + * @param txt 字符串 + * @return {String} + */ + public static String cleanChars(String txt) { + return txt.replaceAll("[  `·•�\\f\\t\\v\\s]", ""); + } + + /** + * 特殊字符正则,sql特殊字符和空白符 + */ + private final static Pattern SPECIAL_CHARS_REGEX = Pattern.compile("[`'\"|/,;()-+*%#·•� \\s]"); + + /** + * 清理字符串,清理出某些不可见字符和一些sql特殊字符 + * + * @param txt 文本 + * @return {String} + */ + @Nullable + public static String cleanText(@Nullable String txt) { + if (txt == null) { + return null; + } + return SPECIAL_CHARS_REGEX.matcher(txt).replaceAll(StringPool.EMPTY); + } + + /** + * 获取标识符,用于参数清理 + * + * @param param 参数 + * @return 清理后的标识符 + */ + @Nullable + public static String cleanIdentifier(@Nullable String param) { + if (param == null) { + return null; + } + StringBuilder paramBuilder = new StringBuilder(); + for (int i = 0; i < param.length(); i++) { + char c = param.charAt(i); + if (Character.isJavaIdentifierPart(c)) { + paramBuilder.append(c); + } + } + return paramBuilder.toString(); + } + + /** + * 随机数生成 + * + * @param count 字符长度 + * @return 随机数 + */ + public static String random(int count) { + return StringUtil.random(count, RandomType.ALL); + } + + /** + * 随机数生成 + * + * @param count 字符长度 + * @param randomType 随机数类别 + * @return 随机数 + */ + public static String random(int count, RandomType randomType) { + if (count == 0) { + return StringPool.EMPTY; + } + Assert.isTrue(count > 0, "Requested random string length " + count + " is less than 0."); + final Random random = Holder.SECURE_RANDOM; + char[] buffer = new char[count]; + for (int i = 0; i < count; i++) { + String factor = randomType.getFactor(); + buffer[i] = factor.charAt(random.nextInt(factor.length())); + } + return new String(buffer); + } + + /** + * 有序的格式化文本,使用{number}做为占位符
+ * 例:
+ * 通常使用:format("this is {0} for {1}", "a", "b") =》 this is a for b
+ * + * @param pattern 文本格式 + * @param arguments 参数 + * @return 格式化后的文本 + */ + public static String indexedFormat(CharSequence pattern, Object... arguments) { + return MessageFormat.format(pattern.toString(), arguments); + } + + /** + * 格式化文本,使用 {varName} 占位
+ * map = {a: "aValue", b: "bValue"} format("{a} and {b}", map) ---=》 aValue and bValue + * + * @param template 文本模板,被替换的部分用 {key} 表示 + * @param map 参数值对 + * @return 格式化后的文本 + */ + public static String format(CharSequence template, Map map) { + if (null == template) { + return null; + } + if (null == map || map.isEmpty()) { + return template.toString(); + } + + String template2 = template.toString(); + for (Map.Entry entry : map.entrySet()) { + template2 = template2.replace("{" + entry.getKey() + "}", Func.toStr(entry.getValue())); + } + return template2; + } + + /** + * 切分字符串,不去除切分后每个元素两边的空白符,不去除空白项 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @param limit 限制分片数,-1不限制 + * @return 切分后的集合 + */ + public static List split(CharSequence str, char separator, int limit) { + return split(str, separator, limit, false, false); + } + + /** + * 分割 字符串 删除常见 空白符 + * + * @param str 字符串 + * @param delimiter 分割符 + * @return 字符串数组 + */ + public static String[] splitTrim(@Nullable String str, @Nullable String delimiter) { + return StringUtil.delimitedListToStringArray(str, delimiter, " \t\n\n\f"); + } + + /** + * 切分字符串,去除切分后每个元素两边的空白符,去除空白项 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @return 切分后的集合 + * @since 3.1.2 + */ + public static List splitTrim(CharSequence str, char separator) { + return splitTrim(str, separator, -1); + } + + /** + * 切分字符串,去除切分后每个元素两边的空白符,去除空白项 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @return 切分后的集合 + * @since 3.2.0 + */ + public static List splitTrim(CharSequence str, CharSequence separator) { + return splitTrim(str, separator, -1); + } + + /** + * 切分字符串,去除切分后每个元素两边的空白符,去除空白项 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @param limit 限制分片数,-1不限制 + * @return 切分后的集合 + * @since 3.1.0 + */ + public static List splitTrim(CharSequence str, char separator, int limit) { + return split(str, separator, limit, true, true); + } + + /** + * 切分字符串,去除切分后每个元素两边的空白符,去除空白项 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @param limit 限制分片数,-1不限制 + * @return 切分后的集合 + * @since 3.2.0 + */ + public static List splitTrim(CharSequence str, CharSequence separator, int limit) { + return split(str, separator, limit, true, true); + } + + /** + * 切分字符串,不限制分片数量 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @param isTrim 是否去除切分字符串后每个元素两边的空格 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static List split(CharSequence str, char separator, boolean isTrim, boolean ignoreEmpty) { + return split(str, separator, 0, isTrim, ignoreEmpty); + } + + /** + * 切分字符串 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @param limit 限制分片数,-1不限制 + * @param isTrim 是否去除切分字符串后每个元素两边的空格 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static List split(CharSequence str, char separator, int limit, boolean isTrim, boolean ignoreEmpty) { + if (null == str) { + return new ArrayList<>(0); + } + return StrSpliter.split(str.toString(), separator, limit, isTrim, ignoreEmpty); + } + + /** + * 切分字符串 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @param limit 限制分片数,-1不限制 + * @param isTrim 是否去除切分字符串后每个元素两边的空格 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.2.0 + */ + public static List split(CharSequence str, CharSequence separator, int limit, boolean isTrim, boolean ignoreEmpty) { + if (null == str) { + return new ArrayList<>(0); + } + final String separatorStr = (null == separator) ? null : separator.toString(); + return StrSpliter.split(str.toString(), separatorStr, limit, isTrim, ignoreEmpty); + } + + /** + * 切分字符串 + * + * @param str 被切分的字符串 + * @param separator 分隔符 + * @return 字符串 + */ + public static String[] split(CharSequence str, CharSequence separator) { + if (str == null) { + return new String[]{}; + } + + final String separatorStr = (null == separator) ? null : separator.toString(); + return StrSpliter.splitToArray(str.toString(), separatorStr, 0, false, false); + } + + /** + * 根据给定长度,将给定字符串截取为多个部分 + * + * @param str 字符串 + * @param len 每一个小节的长度 + * @return 截取后的字符串数组 + * @see StrSpliter#splitByLength(String, int) + */ + public static String[] split(CharSequence str, int len) { + if (null == str) { + return new String[]{}; + } + return StrSpliter.splitByLength(str.toString(), len); + } + + /** + * 指定字符是否在字符串中出现过 + * + * @param str 字符串 + * @param searchChar 被查找的字符 + * @return 是否包含 + * @since 3.1.2 + */ + public static boolean contains(CharSequence str, char searchChar) { + return indexOf(str, searchChar) > -1; + } + + /** + * 查找指定字符串是否包含指定字符串列表中的任意一个字符串 + * + * @param str 指定字符串 + * @param testStrs 需要检查的字符串数组 + * @return 是否包含任意一个字符串 + * @since 3.2.0 + */ + public static boolean containsAny(CharSequence str, CharSequence... testStrs) { + return null != getContainsStr(str, testStrs); + } + + /** + * 查找指定字符串是否包含指定字符串列表中的任意一个字符串,如果包含返回找到的第一个字符串 + * + * @param str 指定字符串 + * @param testStrs 需要检查的字符串数组 + * @return 被包含的第一个字符串 + * @since 3.2.0 + */ + public static String getContainsStr(CharSequence str, CharSequence... testStrs) { + if (ObjectUtil.isEmpty(str) || ObjectUtil.isEmpty(testStrs)) { + return null; + } + for (CharSequence checkStr : testStrs) { + if (str.toString().contains(checkStr)) { + return checkStr.toString(); + } + } + return null; + } + + /** + * 是否包含特定字符,忽略大小写,如果给定两个参数都为null,返回true + * + * @param str 被检测字符串 + * @param testStr 被测试是否包含的字符串 + * @return 是否包含 + */ + public static boolean containsIgnoreCase(CharSequence str, CharSequence testStr) { + if (null == str) { + // 如果被监测字符串和 + return null == testStr; + } + return str.toString().toLowerCase().contains(testStr.toString().toLowerCase()); + } + + /** + * 查找指定字符串是否包含指定字符串列表中的任意一个字符串
+ * 忽略大小写 + * + * @param str 指定字符串 + * @param testStrs 需要检查的字符串数组 + * @return 是否包含任意一个字符串 + * @since 3.2.0 + */ + public static boolean containsAnyIgnoreCase(CharSequence str, CharSequence... testStrs) { + return null != getContainsStrIgnoreCase(str, testStrs); + } + + /** + * 查找指定字符串是否包含指定字符串列表中的任意一个字符串,如果包含返回找到的第一个字符串
+ * 忽略大小写 + * + * @param str 指定字符串 + * @param testStrs 需要检查的字符串数组 + * @return 被包含的第一个字符串 + * @since 3.2.0 + */ + public static String getContainsStrIgnoreCase(CharSequence str, CharSequence... testStrs) { + if (ObjectUtil.isEmpty(str) || Func.isEmpty(testStrs)) { + return null; + } + for (CharSequence testStr : testStrs) { + if (containsIgnoreCase(str, testStr)) { + return testStr.toString(); + } + } + return null; + } + + /** + * 改进JDK subString
+ * index从0开始计算,最后一个字符为-1
+ * 如果from和to位置一样,返回 ""
+ * 如果from或to为负数,则按照length从后向前数位置,如果绝对值大于字符串长度,则from归到0,to归到length
+ * 如果经过修正的index中from大于to,则互换from和to example:
+ * abcdefgh 2 3 =》 c
+ * abcdefgh 2 -3 =》 cde
+ * + * @param str String + * @param fromIndex 开始的index(包括) + * @param toIndex 结束的index(不包括) + * @return 字串 + */ + public static String sub(CharSequence str, int fromIndex, int toIndex) { + if (ObjectUtil.isEmpty(str)) { + return StringPool.EMPTY; + } + int len = str.length(); + + if (fromIndex < 0) { + fromIndex = len + fromIndex; + if (fromIndex < 0) { + fromIndex = 0; + } + } else if (fromIndex > len) { + fromIndex = len; + } + + if (toIndex < 0) { + toIndex = len + toIndex; + if (toIndex < 0) { + toIndex = len; + } + } else if (toIndex > len) { + toIndex = len; + } + + if (toIndex < fromIndex) { + int tmp = fromIndex; + fromIndex = toIndex; + toIndex = tmp; + } + + if (fromIndex == toIndex) { + return StringPool.EMPTY; + } + + return str.toString().substring(fromIndex, toIndex); + } + + + /** + * 截取分隔字符串之前的字符串,不包括分隔字符串
+ * 如果给定的字符串为空串(null或"")或者分隔字符串为null,返回原字符串
+ * 如果分隔字符串为空串"",则返回空串,如果分隔字符串未找到,返回原字符串 + *

+ * 栗子: + * + *

+	 * StringUtil.subBefore(null, *)      = null
+	 * StringUtil.subBefore("", *)        = ""
+	 * StringUtil.subBefore("abc", "a")   = ""
+	 * StringUtil.subBefore("abcba", "b") = "a"
+	 * StringUtil.subBefore("abc", "c")   = "ab"
+	 * StringUtil.subBefore("abc", "d")   = "abc"
+	 * StringUtil.subBefore("abc", "")    = ""
+	 * StringUtil.subBefore("abc", null)  = "abc"
+	 * 
+ * + * @param string 被查找的字符串 + * @param separator 分隔字符串(不包括) + * @param isLastSeparator 是否查找最后一个分隔字符串(多次出现分隔字符串时选取最后一个),true为选取最后一个 + * @return 切割后的字符串 + * @since 3.1.1 + */ + public static String subBefore(CharSequence string, CharSequence separator, boolean isLastSeparator) { + if (ObjectUtil.isEmpty(string) || separator == null) { + return null == string ? null : string.toString(); + } + + final String str = string.toString(); + final String sep = separator.toString(); + if (sep.isEmpty()) { + return StringPool.EMPTY; + } + final int pos = isLastSeparator ? str.lastIndexOf(sep) : str.indexOf(sep); + if (pos == INDEX_NOT_FOUND) { + return str; + } + return str.substring(0, pos); + } + + /** + * 截取分隔字符串之后的字符串,不包括分隔字符串
+ * 如果给定的字符串为空串(null或""),返回原字符串
+ * 如果分隔字符串为空串(null或""),则返回空串,如果分隔字符串未找到,返回空串 + *

+ * 栗子: + * + *

+	 * StringUtil.subAfter(null, *)      = null
+	 * StringUtil.subAfter("", *)        = ""
+	 * StringUtil.subAfter(*, null)      = ""
+	 * StringUtil.subAfter("abc", "a")   = "bc"
+	 * StringUtil.subAfter("abcba", "b") = "cba"
+	 * StringUtil.subAfter("abc", "c")   = ""
+	 * StringUtil.subAfter("abc", "d")   = ""
+	 * StringUtil.subAfter("abc", "")    = "abc"
+	 * 
+ * + * @param string 被查找的字符串 + * @param separator 分隔字符串(不包括) + * @param isLastSeparator 是否查找最后一个分隔字符串(多次出现分隔字符串时选取最后一个),true为选取最后一个 + * @return 切割后的字符串 + * @since 3.1.1 + */ + public static String subAfter(CharSequence string, CharSequence separator, boolean isLastSeparator) { + if (ObjectUtil.isEmpty(string)) { + return null == string ? null : string.toString(); + } + if (separator == null) { + return StringPool.EMPTY; + } + final String str = string.toString(); + final String sep = separator.toString(); + final int pos = isLastSeparator ? str.lastIndexOf(sep) : str.indexOf(sep); + if (pos == INDEX_NOT_FOUND) { + return StringPool.EMPTY; + } + return str.substring(pos + separator.length()); + } + + /** + * 截取指定字符串中间部分,不包括标识字符串
+ *

+ * 栗子: + * + *

+	 * StringUtil.subBetween("wx[b]yz", "[", "]") = "b"
+	 * StringUtil.subBetween(null, *, *)          = null
+	 * StringUtil.subBetween(*, null, *)          = null
+	 * StringUtil.subBetween(*, *, null)          = null
+	 * StringUtil.subBetween("", "", "")          = ""
+	 * StringUtil.subBetween("", "", "]")         = null
+	 * StringUtil.subBetween("", "[", "]")        = null
+	 * StringUtil.subBetween("yabcz", "", "")     = ""
+	 * StringUtil.subBetween("yabcz", "y", "z")   = "abc"
+	 * StringUtil.subBetween("yabczyabcz", "y", "z")   = "abc"
+	 * 
+ * + * @param str 被切割的字符串 + * @param before 截取开始的字符串标识 + * @param after 截取到的字符串标识 + * @return 截取后的字符串 + * @since 3.1.1 + */ + public static String subBetween(CharSequence str, CharSequence before, CharSequence after) { + if (str == null || before == null || after == null) { + return null; + } + + final String str2 = str.toString(); + final String before2 = before.toString(); + final String after2 = after.toString(); + + final int start = str2.indexOf(before2); + if (start != INDEX_NOT_FOUND) { + final int end = str2.indexOf(after2, start + before2.length()); + if (end != INDEX_NOT_FOUND) { + return str2.substring(start + before2.length(), end); + } + } + return null; + } + + /** + * 截取指定字符串中间部分,不包括标识字符串
+ *

+ * 栗子: + * + *

+	 * StringUtil.subBetween(null, *)            = null
+	 * StringUtil.subBetween("", "")             = ""
+	 * StringUtil.subBetween("", "tag")          = null
+	 * StringUtil.subBetween("tagabctag", null)  = null
+	 * StringUtil.subBetween("tagabctag", "")    = ""
+	 * StringUtil.subBetween("tagabctag", "tag") = "abc"
+	 * 
+ * + * @param str 被切割的字符串 + * @param beforeAndAfter 截取开始和结束的字符串标识 + * @return 截取后的字符串 + * @since 3.1.1 + */ + public static String subBetween(CharSequence str, CharSequence beforeAndAfter) { + return subBetween(str, beforeAndAfter, beforeAndAfter); + } + + /** + * 去掉指定前缀 + * + * @param str 字符串 + * @param prefix 前缀 + * @return 切掉后的字符串,若前缀不是 preffix, 返回原字符串 + */ + public static String removePrefix(CharSequence str, CharSequence prefix) { + if (ObjectUtil.isEmpty(str) || ObjectUtil.isEmpty(prefix)) { + return StringPool.EMPTY; + } + + final String str2 = str.toString(); + if (str2.startsWith(prefix.toString())) { + return subSuf(str2, prefix.length()); + } + return str2; + } + + /** + * 忽略大小写去掉指定前缀 + * + * @param str 字符串 + * @param prefix 前缀 + * @return 切掉后的字符串,若前缀不是 prefix, 返回原字符串 + */ + public static String removePrefixIgnoreCase(CharSequence str, CharSequence prefix) { + if (ObjectUtil.isEmpty(str) || ObjectUtil.isEmpty(prefix)) { + return StringPool.EMPTY; + } + + final String str2 = str.toString(); + if (str2.toLowerCase().startsWith(prefix.toString().toLowerCase())) { + return subSuf(str2, prefix.length()); + } + return str2; + } + + /** + * 去掉指定后缀 + * + * @param str 字符串 + * @param suffix 后缀 + * @return 切掉后的字符串,若后缀不是 suffix, 返回原字符串 + */ + public static String removeSuffix(CharSequence str, CharSequence suffix) { + if (ObjectUtil.isEmpty(str) || ObjectUtil.isEmpty(suffix)) { + return StringPool.EMPTY; + } + + final String str2 = str.toString(); + if (str2.endsWith(suffix.toString())) { + return subPre(str2, str2.length() - suffix.length()); + } + return str2; + } + + /** + * 去掉指定后缀,并小写首字母 + * + * @param str 字符串 + * @param suffix 后缀 + * @return 切掉后的字符串,若后缀不是 suffix, 返回原字符串 + */ + public static String removeSufAndLowerFirst(CharSequence str, CharSequence suffix) { + return firstCharToLower(removeSuffix(str, suffix)); + } + + /** + * 忽略大小写去掉指定后缀 + * + * @param str 字符串 + * @param suffix 后缀 + * @return 切掉后的字符串,若后缀不是 suffix, 返回原字符串 + */ + public static String removeSuffixIgnoreCase(CharSequence str, CharSequence suffix) { + if (ObjectUtil.isEmpty(str) || ObjectUtil.isEmpty(suffix)) { + return StringPool.EMPTY; + } + + final String str2 = str.toString(); + if (str2.toLowerCase().endsWith(suffix.toString().toLowerCase())) { + return subPre(str2, str2.length() - suffix.length()); + } + return str2; + } + + /** + * 首字母变小写 + * + * @param str 字符串 + * @return {String} + */ + public static String firstCharToLower(String str) { + char firstChar = str.charAt(0); + if (firstChar >= CharPool.UPPER_A && firstChar <= CharPool.UPPER_Z) { + char[] arr = str.toCharArray(); + arr[0] += (CharPool.LOWER_A - CharPool.UPPER_A); + return new String(arr); + } + return str; + } + + /** + * 首字母变大写 + * + * @param str 字符串 + * @return {String} + */ + public static String firstCharToUpper(String str) { + char firstChar = str.charAt(0); + if (firstChar >= CharPool.LOWER_A && firstChar <= CharPool.LOWER_Z) { + char[] arr = str.toCharArray(); + arr[0] -= (CharPool.LOWER_A - CharPool.UPPER_A); + return new String(arr); + } + return str; + } + + /** + * 切割指定位置之前部分的字符串 + * + * @param string 字符串 + * @param toIndex 切割到的位置(不包括) + * @return 切割后的剩余的前半部分字符串 + */ + public static String subPre(CharSequence string, int toIndex) { + return sub(string, 0, toIndex); + } + + /** + * 切割指定位置之后部分的字符串 + * + * @param string 字符串 + * @param fromIndex 切割开始的位置(包括) + * @return 切割后后剩余的后半部分字符串 + */ + public static String subSuf(CharSequence string, int fromIndex) { + if (ObjectUtil.isEmpty(string)) { + return null; + } + return sub(string, fromIndex, string.length()); + } + + /** + * 指定范围内查找指定字符 + * + * @param str 字符串 + * @param searchChar 被查找的字符 + * @return 位置 + */ + public static int indexOf(final CharSequence str, char searchChar) { + return indexOf(str, searchChar, 0); + } + + /** + * 指定范围内查找指定字符 + * + * @param str 字符串 + * @param searchChar 被查找的字符 + * @param start 起始位置,如果小于0,从0开始查找 + * @return 位置 + */ + public static int indexOf(final CharSequence str, char searchChar, int start) { + if (str instanceof String) { + return ((String) str).indexOf(searchChar, start); + } else { + return indexOf(str, searchChar, start, -1); + } + } + + /** + * 指定范围内查找指定字符 + * + * @param str 字符串 + * @param searchChar 被查找的字符 + * @param start 起始位置,如果小于0,从0开始查找 + * @param end 终止位置,如果超过str.length()则默认查找到字符串末尾 + * @return 位置 + */ + public static int indexOf(final CharSequence str, char searchChar, int start, int end) { + final int len = str.length(); + if (start < 0 || start > len) { + start = 0; + } + if (end > len || end < 0) { + end = len; + } + for (int i = start; i < end; i++) { + if (str.charAt(i) == searchChar) { + return i; + } + } + return -1; + } + + /** + * 指定范围内查找字符串,忽略大小写
+ * + *
+	 * StringUtil.indexOfIgnoreCase(null, *, *)          = -1
+	 * StringUtil.indexOfIgnoreCase(*, null, *)          = -1
+	 * StringUtil.indexOfIgnoreCase("", "", 0)           = 0
+	 * StringUtil.indexOfIgnoreCase("aabaabaa", "A", 0)  = 0
+	 * StringUtil.indexOfIgnoreCase("aabaabaa", "B", 0)  = 2
+	 * StringUtil.indexOfIgnoreCase("aabaabaa", "AB", 0) = 1
+	 * StringUtil.indexOfIgnoreCase("aabaabaa", "B", 3)  = 5
+	 * StringUtil.indexOfIgnoreCase("aabaabaa", "B", 9)  = -1
+	 * StringUtil.indexOfIgnoreCase("aabaabaa", "B", -1) = 2
+	 * StringUtil.indexOfIgnoreCase("aabaabaa", "", 2)   = 2
+	 * StringUtil.indexOfIgnoreCase("abc", "", 9)        = -1
+	 * 
+ * + * @param str 字符串 + * @param searchStr 需要查找位置的字符串 + * @return 位置 + * @since 3.2.1 + */ + public static int indexOfIgnoreCase(final CharSequence str, final CharSequence searchStr) { + return indexOfIgnoreCase(str, searchStr, 0); + } + + /** + * 指定范围内查找字符串 + * + *
+	 * StringUtil.indexOfIgnoreCase(null, *, *)          = -1
+	 * StringUtil.indexOfIgnoreCase(*, null, *)          = -1
+	 * StringUtil.indexOfIgnoreCase("", "", 0)           = 0
+	 * StringUtil.indexOfIgnoreCase("aabaabaa", "A", 0)  = 0
+	 * StringUtil.indexOfIgnoreCase("aabaabaa", "B", 0)  = 2
+	 * StringUtil.indexOfIgnoreCase("aabaabaa", "AB", 0) = 1
+	 * StringUtil.indexOfIgnoreCase("aabaabaa", "B", 3)  = 5
+	 * StringUtil.indexOfIgnoreCase("aabaabaa", "B", 9)  = -1
+	 * StringUtil.indexOfIgnoreCase("aabaabaa", "B", -1) = 2
+	 * StringUtil.indexOfIgnoreCase("aabaabaa", "", 2)   = 2
+	 * StringUtil.indexOfIgnoreCase("abc", "", 9)        = -1
+	 * 
+ * + * @param str 字符串 + * @param searchStr 需要查找位置的字符串 + * @param fromIndex 起始位置 + * @return 位置 + * @since 3.2.1 + */ + public static int indexOfIgnoreCase(final CharSequence str, final CharSequence searchStr, int fromIndex) { + return indexOf(str, searchStr, fromIndex, true); + } + + /** + * 指定范围内反向查找字符串 + * + * @param str 字符串 + * @param searchStr 需要查找位置的字符串 + * @param fromIndex 起始位置 + * @param ignoreCase 是否忽略大小写 + * @return 位置 + * @since 3.2.1 + */ + public static int indexOf(final CharSequence str, CharSequence searchStr, int fromIndex, boolean ignoreCase) { + if (str == null || searchStr == null) { + return INDEX_NOT_FOUND; + } + if (fromIndex < 0) { + fromIndex = 0; + } + + final int endLimit = str.length() - searchStr.length() + 1; + if (fromIndex > endLimit) { + return INDEX_NOT_FOUND; + } + if (searchStr.length() == 0) { + return fromIndex; + } + + if (false == ignoreCase) { + // 不忽略大小写调用JDK方法 + return str.toString().indexOf(searchStr.toString(), fromIndex); + } + + for (int i = fromIndex; i < endLimit; i++) { + if (isSubEquals(str, i, searchStr, 0, searchStr.length(), true)) { + return i; + } + } + return INDEX_NOT_FOUND; + } + + /** + * 指定范围内查找字符串,忽略大小写
+ * + * @param str 字符串 + * @param searchStr 需要查找位置的字符串 + * @return 位置 + * @since 3.2.1 + */ + public static int lastIndexOfIgnoreCase(final CharSequence str, final CharSequence searchStr) { + return lastIndexOfIgnoreCase(str, searchStr, str.length()); + } + + /** + * 指定范围内查找字符串,忽略大小写
+ * + * @param str 字符串 + * @param searchStr 需要查找位置的字符串 + * @param fromIndex 起始位置,从后往前计数 + * @return 位置 + * @since 3.2.1 + */ + public static int lastIndexOfIgnoreCase(final CharSequence str, final CharSequence searchStr, int fromIndex) { + return lastIndexOf(str, searchStr, fromIndex, true); + } + + /** + * 指定范围内查找字符串
+ * + * @param str 字符串 + * @param searchStr 需要查找位置的字符串 + * @param fromIndex 起始位置,从后往前计数 + * @param ignoreCase 是否忽略大小写 + * @return 位置 + * @since 3.2.1 + */ + public static int lastIndexOf(final CharSequence str, final CharSequence searchStr, int fromIndex, boolean ignoreCase) { + if (str == null || searchStr == null) { + return INDEX_NOT_FOUND; + } + if (fromIndex < 0) { + fromIndex = 0; + } + fromIndex = Math.min(fromIndex, str.length()); + + if (searchStr.length() == 0) { + return fromIndex; + } + + if (false == ignoreCase) { + // 不忽略大小写调用JDK方法 + return str.toString().lastIndexOf(searchStr.toString(), fromIndex); + } + + for (int i = fromIndex; i > 0; i--) { + if (isSubEquals(str, i, searchStr, 0, searchStr.length(), true)) { + return i; + } + } + return INDEX_NOT_FOUND; + } + + /** + * 返回字符串 searchStr 在字符串 str 中第 ordinal 次出现的位置。
+ * 此方法来自:Apache-Commons-Lang + *

+ * 栗子(*代表任意字符): + * + *

+	 * StringUtil.ordinalIndexOf(null, *, *)          = -1
+	 * StringUtil.ordinalIndexOf(*, null, *)          = -1
+	 * StringUtil.ordinalIndexOf("", "", *)           = 0
+	 * StringUtil.ordinalIndexOf("aabaabaa", "a", 1)  = 0
+	 * StringUtil.ordinalIndexOf("aabaabaa", "a", 2)  = 1
+	 * StringUtil.ordinalIndexOf("aabaabaa", "b", 1)  = 2
+	 * StringUtil.ordinalIndexOf("aabaabaa", "b", 2)  = 5
+	 * StringUtil.ordinalIndexOf("aabaabaa", "ab", 1) = 1
+	 * StringUtil.ordinalIndexOf("aabaabaa", "ab", 2) = 4
+	 * StringUtil.ordinalIndexOf("aabaabaa", "", 1)   = 0
+	 * StringUtil.ordinalIndexOf("aabaabaa", "", 2)   = 0
+	 * 
+ * + * @param str 被检查的字符串,可以为null + * @param searchStr 被查找的字符串,可以为null + * @param ordinal 第几次出现的位置 + * @return 查找到的位置 + * @since 3.2.3 + */ + public static int ordinalIndexOf(String str, String searchStr, int ordinal) { + if (str == null || searchStr == null || ordinal <= 0) { + return INDEX_NOT_FOUND; + } + if (searchStr.length() == 0) { + return 0; + } + int found = 0; + int index = INDEX_NOT_FOUND; + do { + index = str.indexOf(searchStr, index + 1); + if (index < 0) { + return index; + } + found++; + } while (found < ordinal); + return index; + } + + /** + * 截取两个字符串的不同部分(长度一致),判断截取的子串是否相同
+ * 任意一个字符串为null返回false + * + * @param str1 第一个字符串 + * @param start1 第一个字符串开始的位置 + * @param str2 第二个字符串 + * @param start2 第二个字符串开始的位置 + * @param length 截取长度 + * @param ignoreCase 是否忽略大小写 + * @return 子串是否相同 + * @since 3.2.1 + */ + public static boolean isSubEquals(CharSequence str1, int start1, CharSequence str2, int start2, int length, boolean ignoreCase) { + if (null == str1 || null == str2) { + return false; + } + + return str1.toString().regionMatches(ignoreCase, start1, str2.toString(), start2, length); + } + + /** + * 比较两个字符串(大小写敏感)。 + * + *
+	 * equalsIgnoreCase(null, null)   = true
+	 * equalsIgnoreCase(null, "abc")  = false
+	 * equalsIgnoreCase("abc", null)  = false
+	 * equalsIgnoreCase("abc", "abc") = true
+	 * equalsIgnoreCase("abc", "ABC") = true
+	 * 
+ * + * @param str1 要比较的字符串1 + * @param str2 要比较的字符串2 + * @return 如果两个字符串相同,或者都是null,则返回true + */ + public static boolean equals(CharSequence str1, CharSequence str2) { + return equals(str1, str2, false); + } + + /** + * 比较两个字符串(大小写不敏感)。 + * + *
+	 * equalsIgnoreCase(null, null)   = true
+	 * equalsIgnoreCase(null, "abc")  = false
+	 * equalsIgnoreCase("abc", null)  = false
+	 * equalsIgnoreCase("abc", "abc") = true
+	 * equalsIgnoreCase("abc", "ABC") = true
+	 * 
+ * + * @param str1 要比较的字符串1 + * @param str2 要比较的字符串2 + * @return 如果两个字符串相同,或者都是null,则返回true + */ + public static boolean equalsIgnoreCase(CharSequence str1, CharSequence str2) { + return equals(str1, str2, true); + } + + /** + * 比较两个字符串是否相等。 + * + * @param str1 要比较的字符串1 + * @param str2 要比较的字符串2 + * @param ignoreCase 是否忽略大小写 + * @return 如果两个字符串相同,或者都是null,则返回true + * @since 3.2.0 + */ + public static boolean equals(CharSequence str1, CharSequence str2, boolean ignoreCase) { + if (null == str1) { + // 只有两个都为null才判断相等 + return str2 == null; + } + if (null == str2) { + // 字符串2空,字符串1非空,直接false + return false; + } + + if (ignoreCase) { + return str1.toString().equalsIgnoreCase(str2.toString()); + } else { + return str1.equals(str2); + } + } + + /** + * 创建StringBuilder对象 + * + * @return {String}Builder对象 + */ + public static StringBuilder builder() { + return new StringBuilder(); + } + + /** + * 创建StringBuilder对象 + * + * @param capacity 初始大小 + * @return {String}Builder对象 + */ + public static StringBuilder builder(int capacity) { + return new StringBuilder(capacity); + } + + /** + * 创建StringBuilder对象 + * + * @param strs 初始字符串列表 + * @return {String}Builder对象 + */ + public static StringBuilder builder(CharSequence... strs) { + final StringBuilder sb = new StringBuilder(); + for (CharSequence str : strs) { + sb.append(str); + } + return sb; + } + + /** + * 创建StringBuilder对象 + * + * @param sb 初始StringBuilder + * @param strs 初始字符串列表 + * @return {String}Builder对象 + */ + public static StringBuilder appendBuilder(StringBuilder sb, CharSequence... strs) { + for (CharSequence str : strs) { + sb.append(str); + } + return sb; + } + + /** + * 获得StringReader + * + * @param str 字符串 + * @return {String}Reader + */ + public static StringReader getReader(CharSequence str) { + if (null == str) { + return null; + } + return new StringReader(str.toString()); + } + + /** + * 获得StringWriter + * + * @return {String}Writer + */ + public static StringWriter getWriter() { + return new StringWriter(); + } + + /** + * 统计指定内容中包含指定字符串的数量
+ * 参数为 {@code null} 或者 "" 返回 {@code 0}. + * + *
+	 * StringUtil.count(null, *)       = 0
+	 * StringUtil.count("", *)         = 0
+	 * StringUtil.count("abba", null)  = 0
+	 * StringUtil.count("abba", "")    = 0
+	 * StringUtil.count("abba", "a")   = 2
+	 * StringUtil.count("abba", "ab")  = 1
+	 * StringUtil.count("abba", "xxx") = 0
+	 * 
+ * + * @param content 被查找的字符串 + * @param strForSearch 需要查找的字符串 + * @return 查找到的个数 + */ + public static int count(CharSequence content, CharSequence strForSearch) { + if (Func.hasEmpty(content, strForSearch) || strForSearch.length() > content.length()) { + return 0; + } + + int count = 0; + int idx = 0; + final String content2 = content.toString(); + final String strForSearch2 = strForSearch.toString(); + while ((idx = content2.indexOf(strForSearch2, idx)) > -1) { + count++; + idx += strForSearch.length(); + } + return count; + } + + /** + * 统计指定内容中包含指定字符的数量 + * + * @param content 内容 + * @param charForSearch 被统计的字符 + * @return 包含数量 + */ + public static int count(CharSequence content, char charForSearch) { + int count = 0; + if (ObjectUtil.isEmpty(content)) { + return 0; + } + int contentLength = content.length(); + for (int i = 0; i < contentLength; i++) { + if (charForSearch == content.charAt(i)) { + count++; + } + } + return count; + } + + /** + * 下划线转驼峰 + * + * @param para 字符串 + * @return {String} + */ + public static String underlineToHump(String para) { + if (isBlank(para)) { + return StringPool.EMPTY; + } + StringBuilder result = new StringBuilder(); + String[] a = para.split("_"); + for (String s : a) { + if (result.length() == 0) { + result.append(s.toLowerCase()); + } else { + result.append(s.substring(0, 1).toUpperCase()); + result.append(s.substring(1).toLowerCase()); + } + } + return result.toString(); + } + + /** + * 驼峰转下划线 + * + * @param para 字符串 + * @return {String} + */ + public static String humpToUnderline(String para) { + if (isBlank(para)) { + return StringPool.EMPTY; + } + para = firstCharToLower(para); + StringBuilder sb = new StringBuilder(para); + int temp = 0; + for (int i = 0; i < para.length(); i++) { + if (Character.isUpperCase(para.charAt(i))) { + sb.insert(i + temp, "_"); + temp += 1; + } + } + return sb.toString().toLowerCase(); + } + + /** + * 横线转驼峰 + * + * @param para 字符串 + * @return {String} + */ + public static String lineToHump(String para) { + if (isBlank(para)) { + return StringPool.EMPTY; + } + StringBuilder result = new StringBuilder(); + String[] a = para.split("-"); + for (String s : a) { + if (result.length() == 0) { + result.append(s.toLowerCase()); + } else { + result.append(s.substring(0, 1).toUpperCase()); + result.append(s.substring(1).toLowerCase()); + } + } + return result.toString(); + } + + /** + * 驼峰转横线 + * + * @param para 字符串 + * @return {String} + */ + public static String humpToLine(String para) { + if (isBlank(para)) { + return StringPool.EMPTY; + } + para = firstCharToLower(para); + StringBuilder sb = new StringBuilder(para); + int temp = 0; + for (int i = 0; i < para.length(); i++) { + if (Character.isUpperCase(para.charAt(i))) { + sb.insert(i + temp, "-"); + temp += 1; + } + } + return sb.toString().toLowerCase(); + } + + +} + diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/SuffixFileFilter.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/SuffixFileFilter.java new file mode 100644 index 0000000..61fe460 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/SuffixFileFilter.java @@ -0,0 +1,72 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.tool.utils; + +import org.springframework.util.Assert; + +import java.io.File; +import java.io.FileFilter; +import java.io.Serializable; + +/** + * 文件后缀过滤器 + * + * @author L.cm + */ +public class SuffixFileFilter implements FileFilter, Serializable { + + private static final long serialVersionUID = -3389157631240246157L; + + private final String[] suffixes; + + public SuffixFileFilter(final String suffix) { + Assert.notNull(suffix, "The suffix must not be null"); + this.suffixes = new String[]{suffix}; + } + + public SuffixFileFilter(final String[] suffixes) { + Assert.notNull(suffixes, "The suffix must not be null"); + this.suffixes = new String[suffixes.length]; + System.arraycopy(suffixes, 0, this.suffixes, 0, suffixes.length); + } + + @Override + public boolean accept(File pathname) { + final String name = pathname.getName(); + for (final String suffix : this.suffixes) { + if (checkEndsWith(name, suffix)) { + return true; + } + } + return false; + } + + private boolean checkEndsWith(final String str, final String end) { + final int endLen = end.length(); + return str.regionMatches(true, str.length() - endLen, end, 0, endLen); + } +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/TemplateUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/TemplateUtil.java new file mode 100644 index 0000000..63a065d --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/TemplateUtil.java @@ -0,0 +1,87 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.tool.utils; + + +import org.springblade.core.tool.support.Kv; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * 模版解析工具类 + */ +public class TemplateUtil { + + /** + * 支持 ${} 与 #{} 两种模版占位符 + */ + private static final Pattern pattern = Pattern.compile("\\$\\{([^{}]+)}|\\#\\{([^{}]+)}"); + + /** + * 解析模版 + * + * @param template 模版 + * @param params 参数 + * @return 解析后的字符串 + */ + public static String process(String template, Kv params) { + Matcher matcher = pattern.matcher(template); + StringBuilder sb = new StringBuilder(); + while (matcher.find()) { + String key = matcher.group(1) != null ? matcher.group(1) : matcher.group(2); + String replacement = params.getStr(key); + if (replacement == null) { + throw new IllegalArgumentException("参数中缺少必要的键: " + key); + } + matcher.appendReplacement(sb, replacement); + } + matcher.appendTail(sb); + return sb.toString(); + } + + /** + * 解析模版 + * + * @param template 模版 + * @param params 参数 + * @return 解析后的字符串 + */ + public static String safeProcess(String template, Kv params) { + Matcher matcher = pattern.matcher(template); + StringBuilder sb = new StringBuilder(); + while (matcher.find()) { + String key = matcher.group(1) != null ? matcher.group(1) : matcher.group(2); + String replacement = params.getStr(key); + if (replacement != null) { + matcher.appendReplacement(sb, replacement); + } + } + matcher.appendTail(sb); + return sb.toString(); + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/ThreadLocalUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/ThreadLocalUtil.java new file mode 100644 index 0000000..580b223 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/ThreadLocalUtil.java @@ -0,0 +1,136 @@ +/* + * + * Copyright 2019 http://www.hswebframework.org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package org.springblade.core.tool.utils; + +import org.springframework.lang.Nullable; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Supplier; + +/** + * ThreadLocal 工具类,通过在ThreadLocal存储map信息,来实现在ThreadLocal中维护多个信息 + *
e.g. + * ThreadLocalUtils.put("key",value);
+ * ThreadLocalUtils.get("key");
+ * ThreadLocalUtils.remove("key");
+ * ThreadLocalUtils.getAndRemove("key");
+ * ThreadLocalUtils.get("key",()->defaultValue);
+ * ThreadLocalUtils.clear();
+ *
+ * + * @author zhouhao + * @since 2.0 + */ +@SuppressWarnings("unchecked") +public class ThreadLocalUtil { + private static final ThreadLocal> LOCAL = ThreadLocal.withInitial(HashMap::new); + + /** + * @return threadLocal中的全部值 + */ + public static Map getAll() { + return new HashMap<>(LOCAL.get()); + } + + /** + * 设置一个值到ThreadLocal + * + * @param key 键 + * @param value 值 + * @param 值的类型 + * @return 被放入的值 + * @see Map#put(Object, Object) + */ + public static T put(String key, T value) { + LOCAL.get().put(key, value); + return value; + } + + /** + * 设置一个值到ThreadLocal + * + * @param map map + * @return 被放入的值 + * @see Map#putAll(Map) + */ + public static void put(Map map) { + LOCAL.get().putAll(map); + } + + /** + * 删除参数对应的值 + * + * @param key + * @see Map#remove(Object) + */ + public static void remove(String key) { + LOCAL.get().remove(key); + } + + /** + * 清空ThreadLocal + * + * @see Map#clear() + */ + public static void clear() { + LOCAL.remove(); + } + + /** + * 从ThreadLocal中获取值 + * + * @param key 键 + * @param 值泛型 + * @return 值, 不存在则返回null, 如果类型与泛型不一致, 可能抛出{@link ClassCastException} + * @see Map#get(Object) + * @see ClassCastException + */ + @Nullable + public static T get(String key) { + return ((T) LOCAL.get().get(key)); + } + + /** + * 从ThreadLocal中获取值,并指定一个当值不存在的提供者 + * + * @see Supplier + */ + @Nullable + public static T getIfAbsent(String key, Supplier supplierOnNull) { + return ((T) LOCAL.get().computeIfAbsent(key, k -> supplierOnNull.get())); + } + + /** + * 获取一个值后然后删除掉 + * + * @param key 键 + * @param 值类型 + * @return 值, 不存在则返回null + * @see this#get(String) + * @see this#remove(String) + */ + public static T getAndRemove(String key) { + try { + return get(key); + } finally { + remove(key); + } + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/ThreadUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/ThreadUtil.java new file mode 100644 index 0000000..f52faa5 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/ThreadUtil.java @@ -0,0 +1,64 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.tool.utils; + +import java.util.concurrent.TimeUnit; + +/** + * 多线程工具类 + * + * @author L.cm + */ +public class ThreadUtil { + + /** + * Thread sleep + * + * @param millis 时长 + */ + public static void sleep(long millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + /** + * Thread sleep + * + * @param timeUnit TimeUnit + * @param timeout timeout + */ + public static void sleep(TimeUnit timeUnit, long timeout) { + try { + timeUnit.sleep(timeout); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/Unchecked.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/Unchecked.java new file mode 100644 index 0000000..c33d1c3 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/Unchecked.java @@ -0,0 +1,116 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.tool.utils; + +import org.springblade.core.tool.function.*; + +import java.util.Comparator; +import java.util.Objects; +import java.util.concurrent.Callable; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * Lambda 受检异常处理 + * + *

+ * https://segmentfault.com/a/1190000007832130 + * https://github.com/jOOQ/jOOL + *

+ * + * @author L.cm + */ +public class Unchecked { + + public static Function function(CheckedFunction mapper) { + Objects.requireNonNull(mapper); + return t -> { + try { + return mapper.apply(t); + } catch (Throwable e) { + throw Exceptions.unchecked(e); + } + }; + } + + public static Consumer consumer(CheckedConsumer mapper) { + Objects.requireNonNull(mapper); + return t -> { + try { + mapper.accept(t); + } catch (Throwable e) { + throw Exceptions.unchecked(e); + } + }; + } + + public static Supplier supplier(CheckedSupplier mapper) { + Objects.requireNonNull(mapper); + return () -> { + try { + return mapper.get(); + } catch (Throwable e) { + throw Exceptions.unchecked(e); + } + }; + } + + public static Runnable runnable(CheckedRunnable runnable) { + Objects.requireNonNull(runnable); + return () -> { + try { + runnable.run(); + } catch (Throwable e) { + throw Exceptions.unchecked(e); + } + }; + } + + public static Callable callable(CheckedCallable callable) { + Objects.requireNonNull(callable); + return () -> { + try { + return callable.call(); + } catch (Throwable e) { + throw Exceptions.unchecked(e); + } + }; + } + + public static Comparator comparator(CheckedComparator comparator) { + Objects.requireNonNull(comparator); + return (T o1, T o2) -> { + try { + return comparator.compare(o1, o2); + } catch (Throwable e) { + throw Exceptions.unchecked(e); + } + }; + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/UrlUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/UrlUtil.java new file mode 100644 index 0000000..eb2c674 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/UrlUtil.java @@ -0,0 +1,103 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.tool.utils; + +import org.springframework.util.StringUtils; + +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.Charset; + +/** + * url处理工具类 + * + * @author L.cm + */ +public class UrlUtil extends org.springframework.web.util.UriUtils { + + /** + * url 编码 + * + * @param source source + * @return sourced String + */ + public static String encode(String source) { + return encode(source, Charsets.UTF_8); + } + + /** + * url 解码 + * + * @param source source + * @return decoded String + */ + public static String decode(String source) { + return StringUtils.uriDecode(source, Charsets.UTF_8); + } + + /** + * url 编码 + * + * @param source url + * @param charset 字符集 + * @return 编码后的url + */ + @Deprecated + public static String encodeURL(String source, Charset charset) { + return encode(source, charset.name()); + } + + /** + * url 解码 + * + * @param source url + * @param charset 字符集 + * @return 解码url + */ + @Deprecated + public static String decodeURL(String source, Charset charset) { + return StringUtils.uriDecode(source, charset); + } + + /** + * 获取url路径 + * + * @param uriStr 路径 + * @return url路径 + */ + public static String getPath(String uriStr) { + URI uri; + + try { + uri = new URI(uriStr); + } catch (URISyntaxException var3) { + throw new RuntimeException(var3); + } + + return uri.getPath(); + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/Version.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/Version.java new file mode 100644 index 0000000..3bb0bc9 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/Version.java @@ -0,0 +1,211 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.tool.utils; + +import org.springframework.lang.Nullable; + +/** + * 版本号比较工具 + *

+ * 思路来源于: https://github.com/hotoo/versioning/blob/master/versioning.js + *

+ * example + * * ##完整模式 + * Version.of("v0.1.1").eq("v0.1.2"); // false + *

+ * ##不完整模式 + * Version.of("v0.1").incomplete().eq("v0.1.2"); // true + * + * @author L.cm + * email: 596392912@qq.com + * site:http://www.dreamlu.net + * date 2015年7月9日下午10:48:39 + */ +public class Version { + private static final String DELIMITER = "\\."; + + /** + * 版本号 + */ + @Nullable + private String version; + /** + * 是否完整模式,默认使用完整模式 + */ + private boolean complete = true; + + /** + * 私有实例化构造方法 + */ + private Version() { + } + + private Version(@Nullable String version) { + this.version = version; + } + + /** + * 不完整模式 + * + * @return {Version} + */ + public Version incomplete() { + this.complete = false; + return this; + } + + /** + * 构造器 + * + * @param version 版本 + * @return {Version} + */ + public static Version of(@Nullable String version) { + return new Version(version); + } + + /** + * 比较版本号是否相同 + *

+ * example: + * * Version.of("v0.3").eq("v0.4") + * + * @param version 字符串版本号 + * @return {boolean} + */ + public boolean eq(@Nullable String version) { + return compare(version) == 0; + } + + /** + * 不相同 + *

+ * example: + * * Version.of("v0.3").ne("v0.4") + * + * @param version 字符串版本号 + * @return {boolean} + */ + public boolean ne(@Nullable String version) { + return compare(version) != 0; + } + + /** + * 大于 + * + * @param version 版本号 + * @return 是否大于 + */ + public boolean gt(@Nullable String version) { + return compare(version) > 0; + } + + /** + * 大于和等于 + * + * @param version 版本号 + * @return 是否大于和等于 + */ + public boolean gte(@Nullable String version) { + return compare(version) >= 0; + } + + /** + * 小于 + * + * @param version 版本号 + * @return 是否小于 + */ + public boolean lt(@Nullable String version) { + return compare(version) < 0; + } + + /** + * 小于和等于 + * + * @param version 版本号 + * @return 是否小于和等于 + */ + public boolean lte(@Nullable String version) { + return compare(version) <= 0; + } + + /** + * 和另外一个版本号比较 + * + * @param version 版本号 + * @return {int} + */ + private int compare(@Nullable String version) { + return Version.compare(this.version, version, complete); + } + + /** + * 比较2个版本号 + * + * @param v1 v1 + * @param v2 v2 + * @param complete 是否完整的比较两个版本 + * @return (v1 < v2) ? -1 : ((v1 == v2) ? 0 : 1) + */ + private static int compare(@Nullable String v1, @Nullable String v2, boolean complete) { + // v1 null视为最小版本,排在前 + if (v1 == v2) { + return 0; + } else if (v1 == null) { + return -1; + } else if (v2 == null) { + return 1; + } + // 去除空格 + v1 = v1.trim(); + v2 = v2.trim(); + if (v1.equals(v2)) { + return 0; + } + String[] v1s = v1.split(DELIMITER); + String[] v2s = v2.split(DELIMITER); + int v1sLen = v1s.length; + int v2sLen = v2s.length; + int len = complete + ? Math.max(v1sLen, v2sLen) + : Math.min(v1sLen, v2sLen); + + for (int i = 0; i < len; i++) { + String c1 = len > v1sLen || null == v1s[i] ? "" : v1s[i]; + String c2 = len > v2sLen || null == v2s[i] ? "" : v2s[i]; + + int result = c1.compareTo(c2); + if (result != 0) { + return result; + } + } + + return 0; + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/WebUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/WebUtil.java new file mode 100644 index 0000000..e82a25b --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/WebUtil.java @@ -0,0 +1,323 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.tool.utils; + +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.tool.jackson.JsonUtil; +import org.springframework.http.MediaType; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.method.HandlerMethod; + +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; +import java.util.Enumeration; +import java.util.Objects; +import java.util.function.Predicate; + + +/** + * Miscellaneous utilities for web applications. + * + * @author L.cm + */ +@Slf4j +public class WebUtil extends org.springframework.web.util.WebUtils { + + public static final String USER_AGENT_HEADER = "user-agent"; + + /** + * 判断是否ajax请求 + * spring ajax 返回含有 ResponseBody 或者 RestController注解 + * + * @param handlerMethod HandlerMethod + * @return 是否ajax请求 + */ + public static boolean isBody(HandlerMethod handlerMethod) { + ResponseBody responseBody = ClassUtil.getAnnotation(handlerMethod, ResponseBody.class); + return responseBody != null; + } + + /** + * 读取cookie + * + * @param name cookie name + * @return cookie value + */ + @Nullable + public static String getCookieVal(String name) { + HttpServletRequest request = WebUtil.getRequest(); + Assert.notNull(request, "request from RequestContextHolder is null"); + return getCookieVal(request, name); + } + + /** + * 读取cookie + * + * @param request HttpServletRequest + * @param name cookie name + * @return cookie value + */ + @Nullable + public static String getCookieVal(HttpServletRequest request, String name) { + Cookie cookie = getCookie(request, name); + return cookie != null ? cookie.getValue() : null; + } + + /** + * 清除 某个指定的cookie + * + * @param response HttpServletResponse + * @param key cookie key + */ + public static void removeCookie(HttpServletResponse response, String key) { + setCookie(response, key, null, 0); + } + + /** + * 设置cookie + * + * @param response HttpServletResponse + * @param name cookie name + * @param value cookie value + * @param maxAgeInSeconds maxage + */ + public static void setCookie(HttpServletResponse response, String name, @Nullable String value, int maxAgeInSeconds) { + Cookie cookie = new Cookie(name, value); + cookie.setPath(StringPool.SLASH); + cookie.setMaxAge(maxAgeInSeconds); + cookie.setHttpOnly(true); + response.addCookie(cookie); + } + + /** + * 获取 HttpServletRequest + * + * @return {HttpServletRequest} + */ + public static HttpServletRequest getRequest() { + RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); + return (requestAttributes == null) ? null : ((ServletRequestAttributes) requestAttributes).getRequest(); + } + + /** + * 返回json + * + * @param response HttpServletResponse + * @param result 结果对象 + */ + public static void renderJson(HttpServletResponse response, Object result) { + renderJson(response, result, MediaType.APPLICATION_JSON_VALUE); + } + + /** + * 返回json + * + * @param response HttpServletResponse + * @param result 结果对象 + * @param contentType contentType + */ + public static void renderJson(HttpServletResponse response, Object result, String contentType) { + response.setCharacterEncoding("UTF-8"); + response.setContentType(contentType); + try (PrintWriter out = response.getWriter()) { + out.append(JsonUtil.toJson(result)); + } catch (IOException e) { + log.error(e.getMessage(), e); + } + } + + /** + * 获取ip + * + * @return {String} + */ + public static String getIP() { + return getIP(WebUtil.getRequest()); + } + + private static final String[] IP_HEADER_NAMES = new String[]{ + "x-forwarded-for", + "Proxy-Client-IP", + "WL-Proxy-Client-IP", + "HTTP_CLIENT_IP", + "HTTP_X_FORWARDED_FOR" + }; + + private static final Predicate IP_PREDICATE = (ip) -> StringUtil.isBlank(ip) || StringPool.UNKNOWN.equalsIgnoreCase(ip); + + /** + * 获取ip + * + * @param request HttpServletRequest + * @return {String} + */ + @Nullable + public static String getIP(@Nullable HttpServletRequest request) { + if (request == null) { + return StringPool.EMPTY; + } + String ip = null; + for (String ipHeader : IP_HEADER_NAMES) { + ip = request.getHeader(ipHeader); + if (!IP_PREDICATE.test(ip)) { + break; + } + } + if (IP_PREDICATE.test(ip)) { + ip = request.getRemoteAddr(); + } + return StringUtil.isBlank(ip) ? null : StringUtil.splitTrim(ip, StringPool.COMMA)[0]; + } + + /** + * 获取请求头的值 + * + * @param name 请求头名称 + * @return 请求头 + */ + public static String getHeader(String name) { + HttpServletRequest request = getRequest(); + return Objects.requireNonNull(request).getHeader(name); + } + + /** + * 获取请求头的值 + * + * @param name 请求头名称 + * @return 请求头 + */ + public static Enumeration getHeaders(String name) { + HttpServletRequest request = getRequest(); + return Objects.requireNonNull(request).getHeaders(name); + } + + /** + * 获取所有的请求头 + * + * @return 请求头集合 + */ + public static Enumeration getHeaderNames() { + HttpServletRequest request = getRequest(); + return Objects.requireNonNull(request).getHeaderNames(); + } + + /** + * 获取请求参数 + * + * @param name 请求参数名 + * @return 请求参数 + */ + public static String getParameter(String name) { + HttpServletRequest request = getRequest(); + return Objects.requireNonNull(request).getParameter(name); + } + + /** + * 获取 request 请求体 + * + * @param servletInputStream servletInputStream + * @return body + */ + public static String getRequestBody(ServletInputStream servletInputStream) { + StringBuilder sb = new StringBuilder(); + BufferedReader reader = null; + try { + reader = new BufferedReader(new InputStreamReader(servletInputStream, StandardCharsets.UTF_8)); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line); + } + } catch (IOException e) { + e.printStackTrace(); + } finally { + if (servletInputStream != null) { + try { + servletInputStream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + if (reader != null) { + try { + reader.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + return sb.toString(); + } + + /** + * 获取 request 请求内容 + * + * @param request request + * @return {String} + */ + public static String getRequestContent(HttpServletRequest request) { + try { + String queryString = request.getQueryString(); + if (StringUtil.isNotBlank(queryString)) { + return new String(queryString.getBytes(Charsets.ISO_8859_1), Charsets.UTF_8).replaceAll("&", "&").replaceAll("%22", "\""); + } + String charEncoding = request.getCharacterEncoding(); + if (charEncoding == null) { + charEncoding = StringPool.UTF_8; + } + byte[] buffer = getRequestBody(request.getInputStream()).getBytes(); + String str = new String(buffer, charEncoding).trim(); + if (StringUtil.isBlank(str)) { + StringBuilder sb = new StringBuilder(); + Enumeration parameterNames = request.getParameterNames(); + while (parameterNames.hasMoreElements()) { + String key = parameterNames.nextElement(); + String value = request.getParameter(key); + StringUtil.appendBuilder(sb, key, "=", value, "&"); + } + str = StringUtil.removeSuffix(sb.toString(), "&"); + } + return str.replaceAll("&", "&"); + } catch (Exception ex) { + ex.printStackTrace(); + return StringPool.EMPTY; + } + } + + +} + diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/XmlUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/XmlUtil.java new file mode 100644 index 0000000..acbf2c1 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/XmlUtil.java @@ -0,0 +1,308 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.tool.utils; + +import org.springframework.lang.Nullable; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +import javax.xml.namespace.QName; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; +import java.io.IOException; +import java.io.InputStream; +import java.io.StringReader; +import java.util.HashMap; +import java.util.Map; + +/** + * xpath解析xml + * + *

+ *     文档地址:
+ *     http://www.w3school.com.cn/xpath/index.asp
+ * 
+ * + * @author L.cm + */ +public class XmlUtil { + private final XPath path; + private final Document doc; + + private XmlUtil(InputSource inputSource) throws ParserConfigurationException, SAXException, IOException { + DocumentBuilderFactory dbf = getDocumentBuilderFactory(); + DocumentBuilder db = dbf.newDocumentBuilder(); + doc = db.parse(inputSource); + path = getXPathFactory().newXPath(); + } + + /** + * 创建工具类 + * + * @param inputSource inputSource + * @return XmlUtil + */ + private static XmlUtil create(InputSource inputSource) { + try { + return new XmlUtil(inputSource); + } catch (ParserConfigurationException | SAXException | IOException e) { + throw Exceptions.unchecked(e); + } + } + + /** + * 转换工具类 + * + * @param inputStream inputStream + * @return XmlUtil + */ + public static XmlUtil of(InputStream inputStream) { + InputSource inputSource = new InputSource(inputStream); + return create(inputSource); + } + + /** + * 转换工具类 + * + * @param xmlStr xmlStr + * @return XmlUtil + */ + public static XmlUtil of(String xmlStr) { + StringReader sr = new StringReader(xmlStr.trim()); + InputSource inputSource = new InputSource(sr); + XmlUtil xmlUtil = create(inputSource); + IoUtil.closeQuietly(sr); + return xmlUtil; + } + + /** + * 转换路径 + * + * @param expression 表达式 + * @param item 实体 + * @param returnType 返回类型 + * @return Object + */ + private Object evalXPath(String expression, @Nullable Object item, QName returnType) { + item = null == item ? doc : item; + try { + return path.evaluate(expression, item, returnType); + } catch (XPathExpressionException e) { + throw Exceptions.unchecked(e); + } + } + + /** + * 获取String + * + * @param expression 路径 + * @return {String} + */ + public String getString(String expression) { + return (String) evalXPath(expression, null, XPathConstants.STRING); + } + + /** + * 获取Boolean + * + * @param expression 路径 + * @return {String} + */ + public Boolean getBoolean(String expression) { + return (Boolean) evalXPath(expression, null, XPathConstants.BOOLEAN); + } + + /** + * 获取Number + * + * @param expression 路径 + * @return {Number} + */ + public Number getNumber(String expression) { + return (Number) evalXPath(expression, null, XPathConstants.NUMBER); + } + + /** + * 获取某个节点 + * + * @param expression 路径 + * @return {Node} + */ + public Node getNode(String expression) { + return (Node) evalXPath(expression, null, XPathConstants.NODE); + } + + /** + * 获取子节点 + * + * @param expression 路径 + * @return NodeList + */ + public NodeList getNodeList(String expression) { + return (NodeList) evalXPath(expression, null, XPathConstants.NODESET); + } + + + /** + * 获取String + * + * @param node 节点 + * @param expression 相对于node的路径 + * @return {String} + */ + public String getString(Object node, String expression) { + return (String) evalXPath(expression, node, XPathConstants.STRING); + } + + /** + * 获取 + * + * @param node 节点 + * @param expression 相对于node的路径 + * @return {String} + */ + public Boolean getBoolean(Object node, String expression) { + return (Boolean) evalXPath(expression, node, XPathConstants.BOOLEAN); + } + + /** + * 获取 + * + * @param node 节点 + * @param expression 相对于node的路径 + * @return {Number} + */ + public Number getNumber(Object node, String expression) { + return (Number) evalXPath(expression, node, XPathConstants.NUMBER); + } + + /** + * 获取某个节点 + * + * @param node 节点 + * @param expression 路径 + * @return {Node} + */ + public Node getNode(Object node, String expression) { + return (Node) evalXPath(expression, node, XPathConstants.NODE); + } + + /** + * 获取子节点 + * + * @param node 节点 + * @param expression 相对于node的路径 + * @return NodeList + */ + public NodeList getNodeList(Object node, String expression) { + return (NodeList) evalXPath(expression, node, XPathConstants.NODESET); + } + + /** + * 针对没有嵌套节点的简单处理 + * + * @return map集合 + */ + public Map toMap() { + Element root = doc.getDocumentElement(); + Map params = new HashMap<>(16); + + // 将节点封装成map形式 + NodeList list = root.getChildNodes(); + for (int i = 0; i < list.getLength(); i++) { + Node node = list.item(i); + if (node instanceof Element) { + params.put(node.getNodeName(), node.getTextContent()); + } + } + return params; + } + + private static volatile boolean preventedXXE = false; + + private static DocumentBuilderFactory getDocumentBuilderFactory() throws ParserConfigurationException { + DocumentBuilderFactory dbf = XmlUtil.XmlHelperHolder.documentBuilderFactory; + if (!preventedXXE) { + preventXXE(dbf); + } + return dbf; + } + + /** + * preventXXE + * + * @param dbf + * @throws ParserConfigurationException + */ + private static void preventXXE(DocumentBuilderFactory dbf) throws ParserConfigurationException { + // This is the PRIMARY defense. If DTDs (doctypes) are disallowed, almost all XML entity attacks are prevented + // Xerces 2 only - http://xerces.apache.org/xerces2-j/features.html#disallow-doctype-decl + dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + + // If you can't completely disable DTDs, then at least do the following: + // Xerces 1 - http://xerces.apache.org/xerces-j/features.html#external-general-entities + // Xerces 2 - http://xerces.apache.org/xerces2-j/features.html#external-general-entities + + // JDK7+ - http://xml.org/sax/features/external-general-entities + dbf.setFeature("http://xml.org/sax/features/external-general-entities", false); + + // Xerces 1 - http://xerces.apache.org/xerces-j/features.html#external-parameter-entities + // Xerces 2 - http://xerces.apache.org/xerces2-j/features.html#external-parameter-entities + + // JDK7+ - http://xml.org/sax/features/external-parameter-entities + dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + + // Disable external DTDs as well + dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); + + // and these as well, per Timothy Morgan's 2014 paper: "XML Schema, DTD, and Entity Attacks" + dbf.setXIncludeAware(false); + dbf.setExpandEntityReferences(false); + preventedXXE = true; + } + + private static XPathFactory getXPathFactory() { + return XmlUtil.XmlHelperHolder.xPathFactory; + } + + /** + * 内部类单例 + */ + private static class XmlHelperHolder { + private static DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); + private static XPathFactory xPathFactory = XPathFactory.newInstance(); + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/yml/YmlPropertyLoaderFactory.java b/blade-core-tool/src/main/java/org/springblade/core/tool/yml/YmlPropertyLoaderFactory.java new file mode 100644 index 0000000..65c5a08 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/yml/YmlPropertyLoaderFactory.java @@ -0,0 +1,82 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.tool.yml; + +import org.springblade.core.tool.utils.StringUtil; +import org.springframework.boot.env.OriginTrackedMapPropertySource; +import org.springframework.boot.env.YamlPropertySourceLoader; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.PropertySource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.DefaultPropertySourceFactory; +import org.springframework.core.io.support.EncodedResource; +import org.springframework.lang.Nullable; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +/** + * yml配置加载 + * + * @author lcm + */ +public class YmlPropertyLoaderFactory extends DefaultPropertySourceFactory { + + @Override + public PropertySource createPropertySource(@Nullable String name, EncodedResource encodedResource) throws IOException { + if (encodedResource == null) { + return emptyPropertySource(name); + } + Resource resource = encodedResource.getResource(); + String fileName = resource.getFilename(); + List> sources = new YamlPropertySourceLoader().load(fileName, resource); + if (sources.isEmpty()) { + return emptyPropertySource(fileName); + } + // yml 数据存储,合成一个 PropertySource + Map ymlDataMap = new HashMap<>(32); + for (PropertySource source : sources) { + ymlDataMap.putAll(((MapPropertySource) source).getSource()); + } + return new OriginTrackedMapPropertySource(getSourceName(fileName, name), ymlDataMap); + } + + private static PropertySource emptyPropertySource(@Nullable String name) { + return new MapPropertySource(getSourceName(name), Collections.emptyMap()); + } + + private static String getSourceName(String... names) { + return Stream.of(names) + .filter(StringUtil::isNotBlank) + .findFirst() + .orElse("BladeYmlPropertySource"); + } + +} diff --git a/blade-starter-actuate/README.md b/blade-starter-actuate/README.md new file mode 100644 index 0000000..57d34d5 --- /dev/null +++ b/blade-starter-actuate/README.md @@ -0,0 +1,34 @@ +## 想法 +暴露一些端点,提供一些功能。 + +1. http-cache + +2. RateLimiter + +3. ... ... + +### 不是用网关,单体应用 +拦截器处理,基于 redis 的 cache 时间或者 RateLimiter处理。 + +结构:serviceName:http-cache:/user/1?queryString If-Modified-Since +结构:serviceName:RateLimiter:/user/1 99 + +### 使用网关 +将端点信息存储到 redis 里,供 网关使用。 +结构:serviceName:http-cache:endpoint:/user/{id} 100s + +结构:serviceName:RateLimiter:endpoint:/user/{id} 100/s + +## RateLimiter Headers +```text +#=============================#===================================================# +# HTTP Header # Description # +#=============================#===================================================# +| X-RateLimit-Limit | Request limit per day / per 5 minutes | ++-----------------------------+---------------------------------------------------+ +| X-RateLimit-Remaining | The number of requests left for the time window | ++-----------------------------+---------------------------------------------------+ +| X-RateLimit-Reset | The remaining window before the rate limit resets | +| | in UTC epoch seconds | ++-----------------------------+---------------------------------------------------+ +``` \ No newline at end of file diff --git a/blade-starter-actuate/pom.xml b/blade-starter-actuate/pom.xml new file mode 100644 index 0000000..f581a16 --- /dev/null +++ b/blade-starter-actuate/pom.xml @@ -0,0 +1,30 @@ + + + 4.0.0 + + BladeX-Tool + org.springblade + ${revision} + + + blade-starter-actuate + ${project.artifactId} + ${project.parent.version} + jar + + + + org.springblade + blade-core-tool + + + + org.springblade + blade-core-auto + provided + + + + diff --git a/blade-starter-actuate/src/main/java/org/springblade/core/http/cache/BladeHttpCacheProperties.java b/blade-starter-actuate/src/main/java/org/springblade/core/http/cache/BladeHttpCacheProperties.java new file mode 100644 index 0000000..046f082 --- /dev/null +++ b/blade-starter-actuate/src/main/java/org/springblade/core/http/cache/BladeHttpCacheProperties.java @@ -0,0 +1,61 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.http.cache; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.ArrayList; +import java.util.List; + +/** + * Http Cache 配置 + * + * @author L.cm + */ +@ConfigurationProperties("blade.http.cache") +public class BladeHttpCacheProperties { + /** + * Http-cache 的 spring cache名,默认:bladeHttpCache + */ + @Getter + @Setter + private String cacheName = "bladeHttpCache"; + /** + * 默认拦截/** + */ + @Getter + private final List includePatterns = new ArrayList() {{ + add("/**"); + }}; + /** + * 默认排除静态文件目录 + */ + @Getter + private final List excludePatterns = new ArrayList<>(); +} diff --git a/blade-starter-actuate/src/main/java/org/springblade/core/http/cache/HttpCacheAble.java b/blade-starter-actuate/src/main/java/org/springblade/core/http/cache/HttpCacheAble.java new file mode 100644 index 0000000..cff180a --- /dev/null +++ b/blade-starter-actuate/src/main/java/org/springblade/core/http/cache/HttpCacheAble.java @@ -0,0 +1,63 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.http.cache; + +import org.springframework.core.annotation.AliasFor; + +import java.lang.annotation.*; + +/** + * Http cache + * cache-control + *

+ * max-age 大于0 时 直接从游览器缓存中 提取 + * max-age 小于或等于0 时 向server 发送http 请求确认 ,该资源是否有修改 + * + * @author L.cm + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface HttpCacheAble { + + /** + * 缓存的时间,默认0,单位秒 + * + * @return {long} + */ + @AliasFor("maxAge") + long value(); + + /** + * 缓存的时间,默认0,单位秒 + * + * @return {long} + */ + @AliasFor("value") + long maxAge() default 0; + +} diff --git a/blade-starter-actuate/src/main/java/org/springblade/core/http/cache/HttpCacheConfiguration.java b/blade-starter-actuate/src/main/java/org/springblade/core/http/cache/HttpCacheConfiguration.java new file mode 100644 index 0000000..e15daab --- /dev/null +++ b/blade-starter-actuate/src/main/java/org/springblade/core/http/cache/HttpCacheConfiguration.java @@ -0,0 +1,76 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.http.cache; + +import lombok.AllArgsConstructor; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.web.servlet.WebMvcProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cache.CacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.lang.NonNull; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.HashSet; +import java.util.Set; + +/** + * Http Cache 配置 + * + * @author L.cm + */ +@AutoConfiguration +@AllArgsConstructor +@EnableConfigurationProperties(BladeHttpCacheProperties.class) +@ConditionalOnProperty(value = "blade.http.cache.enabled", havingValue = "true") +public class HttpCacheConfiguration implements WebMvcConfigurer { + private static final String DEFAULT_STATIC_PATH_PATTERN = "/**"; + private final WebMvcProperties webMvcProperties; + private final BladeHttpCacheProperties properties; + private final CacheManager cacheManager; + + @Bean + public HttpCacheService httpCacheService() { + return new HttpCacheService(properties, cacheManager); + } + + @Override + public void addInterceptors(@NonNull InterceptorRegistry registry) { + Set excludePatterns = new HashSet<>(properties.getExcludePatterns()); + String staticPathPattern = webMvcProperties.getStaticPathPattern(); + // 如果静态 目录 不为 /** + if (!DEFAULT_STATIC_PATH_PATTERN.equals(staticPathPattern.trim())) { + excludePatterns.add(staticPathPattern); + } + HttpCacheInterceptor httpCacheInterceptor = new HttpCacheInterceptor(httpCacheService()); + registry.addInterceptor(httpCacheInterceptor) + .addPathPatterns(properties.getIncludePatterns().toArray(new String[0])) + .excludePathPatterns(excludePatterns.toArray(new String[0])); + } +} diff --git a/blade-starter-actuate/src/main/java/org/springblade/core/http/cache/HttpCacheInterceptor.java b/blade-starter-actuate/src/main/java/org/springblade/core/http/cache/HttpCacheInterceptor.java new file mode 100644 index 0000000..74d6ed7 --- /dev/null +++ b/blade-starter-actuate/src/main/java/org/springblade/core/http/cache/HttpCacheInterceptor.java @@ -0,0 +1,100 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.http.cache; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.tool.utils.ClassUtil; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.lang.NonNull; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; + +import java.time.Clock; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * Http cache拦截器 + * + * @author L.cm + */ +@Slf4j +@AllArgsConstructor +public class HttpCacheInterceptor implements HandlerInterceptor { + private final HttpCacheService httpCacheService; + + @Override + public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) throws Exception { + // 非控制器请求直接跳出 + if (!(handler instanceof HandlerMethod handlerMethod)) { + return true; + } + // http cache 针对 HEAD 和 GET 请求 + String method = request.getMethod(); + HttpMethod httpMethod = HttpMethod.valueOf(method); + List allowList = Arrays.asList(HttpMethod.HEAD, HttpMethod.GET); + if (!allowList.contains(httpMethod)) { + return true; + } + // 处理HttpCacheAble + HttpCacheAble cacheAble = ClassUtil.getAnnotation(handlerMethod, HttpCacheAble.class); + if (cacheAble == null) { + return true; + } + + // 最后修改时间 + long ims = request.getDateHeader(HttpHeaders.IF_MODIFIED_SINCE); + long now = Clock.systemUTC().millis(); + // 缓存时间,秒 + long maxAge = cacheAble.maxAge(); + // 缓存时间,毫秒 + long maxAgeMicros = TimeUnit.SECONDS.toMillis(maxAge); + String cacheKey = request.getRequestURI() + "?" + request.getQueryString(); + // 后端可控制http-cache超时 + boolean hasCache = httpCacheService.get(cacheKey); + // 如果header头没有过期 + if (hasCache && ims + maxAgeMicros > now) { + response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); + response.setHeader(HttpHeaders.CACHE_CONTROL, "max-age=" + maxAge); + response.addDateHeader(HttpHeaders.EXPIRES, ims + maxAgeMicros); + response.addDateHeader(HttpHeaders.LAST_MODIFIED, ims); + log.info("{} 304 {}", method, request.getRequestURI()); + return false; + } + response.setHeader(HttpHeaders.CACHE_CONTROL, "max-age=" + maxAge); + response.addDateHeader(HttpHeaders.EXPIRES, now + maxAgeMicros); + response.addDateHeader(HttpHeaders.LAST_MODIFIED, now); + httpCacheService.set(cacheKey); + return true; + } + +} diff --git a/blade-starter-actuate/src/main/java/org/springblade/core/http/cache/HttpCacheService.java b/blade-starter-actuate/src/main/java/org/springblade/core/http/cache/HttpCacheService.java new file mode 100644 index 0000000..2ad4a58 --- /dev/null +++ b/blade-starter-actuate/src/main/java/org/springblade/core/http/cache/HttpCacheService.java @@ -0,0 +1,73 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.http.cache; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.util.Assert; + +/** + * Http Cache 服务 + * + * @author L.cm + */ +public class HttpCacheService implements InitializingBean { + private final BladeHttpCacheProperties properties; + private final CacheManager cacheManager; + private Cache cache; + + public HttpCacheService(BladeHttpCacheProperties properties, CacheManager cacheManager) { + this.properties = properties; + this.cacheManager = cacheManager; + } + + public boolean get(String key) { + Boolean result = cache.get(key, Boolean.class); + return Boolean.TRUE.equals(result); + } + + public void set(String key) { + cache.put(key, Boolean.TRUE); + } + + public void remove(String key) { + cache.evict(key); + } + + public void clear() { + cache.clear(); + } + + @Override + public void afterPropertiesSet() throws Exception { + Assert.notNull(cacheManager, "cacheManager must not be null!"); + String cacheName = properties.getCacheName(); + this.cache = cacheManager.getCache(cacheName); + Assert.notNull(this.cache, "HttpCacheCache cacheName: " + cacheName + " is not config."); + } +} diff --git a/blade-starter-api-crypto/README.md b/blade-starter-api-crypto/README.md new file mode 100644 index 0000000..1d0eac3 --- /dev/null +++ b/blade-starter-api-crypto/README.md @@ -0,0 +1,4 @@ +## 参考 +encrypt-body-spring-boot-starter: https://github.com/Licoy/encrypt-body-spring-boot-starter + +mica:https://github.com/lets-mica/mica \ No newline at end of file diff --git a/blade-starter-api-crypto/pom.xml b/blade-starter-api-crypto/pom.xml new file mode 100644 index 0000000..72b1a6f --- /dev/null +++ b/blade-starter-api-crypto/pom.xml @@ -0,0 +1,34 @@ + + + 4.0.0 + + BladeX-Tool + org.springblade + ${revision} + + + blade-starter-api-crypto + ${project.artifactId} + ${project.parent.version} + jar + + + + org.springblade + blade-core-tool + + + org.springblade + blade-core-auto + provided + + + org.springframework.cloud + spring-cloud-context + + + + + diff --git a/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/annotation/crypto/ApiCrypto.java b/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/annotation/crypto/ApiCrypto.java new file mode 100644 index 0000000..3ad6617 --- /dev/null +++ b/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/annotation/crypto/ApiCrypto.java @@ -0,0 +1,22 @@ +package org.springblade.core.api.crypto.annotation.crypto; + +import org.springblade.core.api.crypto.annotation.decrypt.ApiDecrypt; +import org.springblade.core.api.crypto.annotation.encrypt.ApiEncrypt; +import org.springblade.core.api.crypto.enums.CryptoType; + +import java.lang.annotation.*; + +/** + *

AES加密解密含有{@link org.springframework.web.bind.annotation.RequestBody}注解的参数请求数据

+ * + * @author Chill + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@ApiEncrypt(CryptoType.AES) +@ApiDecrypt(CryptoType.AES) +public @interface ApiCrypto { + +} diff --git a/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/annotation/crypto/ApiCryptoAes.java b/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/annotation/crypto/ApiCryptoAes.java new file mode 100644 index 0000000..c7aa98f --- /dev/null +++ b/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/annotation/crypto/ApiCryptoAes.java @@ -0,0 +1,22 @@ +package org.springblade.core.api.crypto.annotation.crypto; + +import org.springblade.core.api.crypto.annotation.decrypt.ApiDecrypt; +import org.springblade.core.api.crypto.annotation.encrypt.ApiEncrypt; +import org.springblade.core.api.crypto.enums.CryptoType; + +import java.lang.annotation.*; + +/** + *

AES加密解密含有{@link org.springframework.web.bind.annotation.RequestBody}注解的参数请求数据

+ * + * @author Chill + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@ApiEncrypt(CryptoType.AES) +@ApiDecrypt(CryptoType.AES) +public @interface ApiCryptoAes { + +} diff --git a/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/annotation/crypto/ApiCryptoDes.java b/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/annotation/crypto/ApiCryptoDes.java new file mode 100644 index 0000000..6ea127a --- /dev/null +++ b/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/annotation/crypto/ApiCryptoDes.java @@ -0,0 +1,22 @@ +package org.springblade.core.api.crypto.annotation.crypto; + +import org.springblade.core.api.crypto.annotation.decrypt.ApiDecrypt; +import org.springblade.core.api.crypto.annotation.encrypt.ApiEncrypt; +import org.springblade.core.api.crypto.enums.CryptoType; + +import java.lang.annotation.*; + +/** + *

DES加密解密含有{@link org.springframework.web.bind.annotation.RequestBody}注解的参数请求数据

+ * + * @author Chill + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@ApiEncrypt(CryptoType.DES) +@ApiDecrypt(CryptoType.DES) +public @interface ApiCryptoDes { + +} diff --git a/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/annotation/crypto/ApiCryptoRsa.java b/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/annotation/crypto/ApiCryptoRsa.java new file mode 100644 index 0000000..6f4ba55 --- /dev/null +++ b/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/annotation/crypto/ApiCryptoRsa.java @@ -0,0 +1,22 @@ +package org.springblade.core.api.crypto.annotation.crypto; + +import org.springblade.core.api.crypto.annotation.decrypt.ApiDecrypt; +import org.springblade.core.api.crypto.annotation.encrypt.ApiEncrypt; +import org.springblade.core.api.crypto.enums.CryptoType; + +import java.lang.annotation.*; + +/** + *

RSA加密解密含有{@link org.springframework.web.bind.annotation.RequestBody}注解的参数请求数据

+ * + * @author Chill + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@ApiEncrypt(CryptoType.RSA) +@ApiDecrypt(CryptoType.RSA) +public @interface ApiCryptoRsa { + +} diff --git a/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/annotation/decrypt/ApiDecrypt.java b/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/annotation/decrypt/ApiDecrypt.java new file mode 100644 index 0000000..6defbd0 --- /dev/null +++ b/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/annotation/decrypt/ApiDecrypt.java @@ -0,0 +1,32 @@ +package org.springblade.core.api.crypto.annotation.decrypt; + +import org.springblade.core.api.crypto.enums.CryptoType; + +import java.lang.annotation.*; + +/** + *

解密含有{@link org.springframework.web.bind.annotation.RequestBody}注解的参数请求数据,可用于整个控制类或者某个控制器上

+ * + * @author licoy.cn, L.cm + */ +@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface ApiDecrypt { + + /** + * 解密类型 + * + * @return 类型 + */ + CryptoType value(); + + /** + * 私钥,用于某些需要单独配置私钥的方法,没有时读取全局配置的私钥 + * + * @return 私钥 + */ + String secretKey() default ""; + +} diff --git a/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/annotation/decrypt/ApiDecryptAes.java b/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/annotation/decrypt/ApiDecryptAes.java new file mode 100644 index 0000000..7dfcbb4 --- /dev/null +++ b/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/annotation/decrypt/ApiDecryptAes.java @@ -0,0 +1,28 @@ +package org.springblade.core.api.crypto.annotation.decrypt; + +import org.springblade.core.api.crypto.enums.CryptoType; +import org.springframework.core.annotation.AliasFor; + +import java.lang.annotation.*; + +/** + * aes 解密 + * + * @author licoy.cn, L.cm + * @see ApiDecrypt + */ +@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@ApiDecrypt(CryptoType.AES) +public @interface ApiDecryptAes { + + /** + * Alias for {@link ApiDecrypt#secretKey()}. + * + * @return {String} + */ + @AliasFor(annotation = ApiDecrypt.class) + String secretKey() default ""; + +} diff --git a/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/annotation/decrypt/ApiDecryptDes.java b/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/annotation/decrypt/ApiDecryptDes.java new file mode 100644 index 0000000..d2efc7b --- /dev/null +++ b/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/annotation/decrypt/ApiDecryptDes.java @@ -0,0 +1,28 @@ +package org.springblade.core.api.crypto.annotation.decrypt; + +import org.springblade.core.api.crypto.enums.CryptoType; +import org.springframework.core.annotation.AliasFor; + +import java.lang.annotation.*; + +/** + * des 解密 + * + * @author licoy.cn + * @see ApiDecrypt + */ +@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@ApiDecrypt(CryptoType.DES) +public @interface ApiDecryptDes { + + /** + * Alias for {@link ApiDecrypt#secretKey()}. + * + * @return {String} + */ + @AliasFor(annotation = ApiDecrypt.class) + String secretKey() default ""; + +} diff --git a/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/annotation/decrypt/ApiDecryptRsa.java b/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/annotation/decrypt/ApiDecryptRsa.java new file mode 100644 index 0000000..73a486d --- /dev/null +++ b/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/annotation/decrypt/ApiDecryptRsa.java @@ -0,0 +1,18 @@ +package org.springblade.core.api.crypto.annotation.decrypt; + +import org.springblade.core.api.crypto.enums.CryptoType; + +import java.lang.annotation.*; + +/** + * rsa 解密 + * + * @author licoy.cn + * @see ApiDecrypt + */ +@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@ApiDecrypt(CryptoType.RSA) +public @interface ApiDecryptRsa { +} diff --git a/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/annotation/encrypt/ApiEncrypt.java b/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/annotation/encrypt/ApiEncrypt.java new file mode 100644 index 0000000..fb50bd3 --- /dev/null +++ b/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/annotation/encrypt/ApiEncrypt.java @@ -0,0 +1,33 @@ +package org.springblade.core.api.crypto.annotation.encrypt; + + +import org.springblade.core.api.crypto.enums.CryptoType; + +import java.lang.annotation.*; + +/** + *

加密{@link org.springframework.web.bind.annotation.ResponseBody}响应数据,可用于整个控制类或者某个控制器上

+ * + * @author licoy.cn, L.cm + */ +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface ApiEncrypt { + + /** + * 加密类型 + * + * @return 类型 + */ + CryptoType value(); + + /** + * 私钥,用于某些需要单独配置私钥的方法,没有时读取全局配置的私钥 + * + * @return 私钥 + */ + String secretKey() default ""; + +} diff --git a/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/annotation/encrypt/ApiEncryptAes.java b/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/annotation/encrypt/ApiEncryptAes.java new file mode 100644 index 0000000..2b45790 --- /dev/null +++ b/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/annotation/encrypt/ApiEncryptAes.java @@ -0,0 +1,28 @@ +package org.springblade.core.api.crypto.annotation.encrypt; + +import org.springblade.core.api.crypto.enums.CryptoType; +import org.springframework.core.annotation.AliasFor; + +import java.lang.annotation.*; + +/** + * aes 加密 + * + * @author licoy.cn, L.cm + * @see ApiEncrypt + */ +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@ApiEncrypt(CryptoType.AES) +public @interface ApiEncryptAes { + + /** + * Alias for {@link ApiEncrypt#secretKey()}. + * + * @return {String} + */ + @AliasFor(annotation = ApiEncrypt.class) + String secretKey() default ""; + +} diff --git a/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/annotation/encrypt/ApiEncryptDes.java b/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/annotation/encrypt/ApiEncryptDes.java new file mode 100644 index 0000000..036de8a --- /dev/null +++ b/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/annotation/encrypt/ApiEncryptDes.java @@ -0,0 +1,28 @@ +package org.springblade.core.api.crypto.annotation.encrypt; + +import org.springblade.core.api.crypto.enums.CryptoType; +import org.springframework.core.annotation.AliasFor; + +import java.lang.annotation.*; + +/** + * des 加密 + * + * @author licoy.cn, L.cm + * @see ApiEncrypt + */ +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@ApiEncrypt(CryptoType.DES) +public @interface ApiEncryptDes { + + /** + * Alias for {@link ApiEncrypt#secretKey()}. + * + * @return {String} + */ + @AliasFor(annotation = ApiEncrypt.class) + String secretKey() default ""; + +} diff --git a/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/annotation/encrypt/ApiEncryptRsa.java b/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/annotation/encrypt/ApiEncryptRsa.java new file mode 100644 index 0000000..e36d9d8 --- /dev/null +++ b/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/annotation/encrypt/ApiEncryptRsa.java @@ -0,0 +1,18 @@ +package org.springblade.core.api.crypto.annotation.encrypt; + +import org.springblade.core.api.crypto.enums.CryptoType; + +import java.lang.annotation.*; + +/** + * rsa 加密 + * + * @author licoy.cn, L.cm + * @see ApiEncrypt + */ +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@ApiEncrypt(CryptoType.RSA) +public @interface ApiEncryptRsa { +} diff --git a/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/bean/CryptoInfoBean.java b/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/bean/CryptoInfoBean.java new file mode 100644 index 0000000..e1e6d3f --- /dev/null +++ b/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/bean/CryptoInfoBean.java @@ -0,0 +1,25 @@ +package org.springblade.core.api.crypto.bean; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springblade.core.api.crypto.enums.CryptoType; + +/** + *

加密注解信息

+ * + * @author licoy.cn, L.cm + */ +@Getter +@RequiredArgsConstructor +public class CryptoInfoBean { + + /** + * 加密类型 + */ + private final CryptoType type; + /** + * 私钥 + */ + private final String secretKey; + +} diff --git a/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/bean/DecryptHttpInputMessage.java b/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/bean/DecryptHttpInputMessage.java new file mode 100644 index 0000000..8c23fb3 --- /dev/null +++ b/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/bean/DecryptHttpInputMessage.java @@ -0,0 +1,20 @@ +package org.springblade.core.api.crypto.bean; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpInputMessage; + +import java.io.InputStream; + +/** + *

解密信息输入流

+ * + * @author licoy.cn, L.cm + */ +@Getter +@RequiredArgsConstructor +public class DecryptHttpInputMessage implements HttpInputMessage { + private final InputStream body; + private final HttpHeaders headers; +} diff --git a/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/config/ApiCryptoConfiguration.java b/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/config/ApiCryptoConfiguration.java new file mode 100644 index 0000000..c683f44 --- /dev/null +++ b/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/config/ApiCryptoConfiguration.java @@ -0,0 +1,30 @@ +package org.springblade.core.api.crypto.config; + +import lombok.RequiredArgsConstructor; +import org.springblade.core.api.crypto.core.ApiDecryptParamResolver; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.List; + +/** + * api 签名自动配置 + * + * @author L.cm + */ +@AutoConfiguration +@RequiredArgsConstructor +@EnableConfigurationProperties(ApiCryptoProperties.class) +@ConditionalOnProperty(value = ApiCryptoProperties.PREFIX + ".enabled", havingValue = "true", matchIfMissing = true) +public class ApiCryptoConfiguration implements WebMvcConfigurer { + private final ApiCryptoProperties apiCryptoProperties; + + @Override + public void addArgumentResolvers(List argumentResolvers) { + argumentResolvers.add(new ApiDecryptParamResolver(apiCryptoProperties)); + } + +} diff --git a/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/config/ApiCryptoProperties.java b/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/config/ApiCryptoProperties.java new file mode 100644 index 0000000..e548359 --- /dev/null +++ b/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/config/ApiCryptoProperties.java @@ -0,0 +1,46 @@ +package org.springblade.core.api.crypto.config; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * api 签名配置类 + * + * @author licoy.cn, L.cm + */ +@Getter +@Setter +@ConfigurationProperties(ApiCryptoProperties.PREFIX) +public class ApiCryptoProperties { + /** + * 前缀 + */ + public static final String PREFIX = "blade.api.crypto"; + + /** + * 是否开启 api 签名 + */ + private Boolean enabled = Boolean.TRUE; + + /** + * url的参数签名,传递的参数名。例如:/user?data=签名后的数据 + */ + private String paramName = "data"; + + /** + * aes 密钥 + */ + private String aesKey; + + /** + * des 密钥 + */ + private String desKey; + + /** + * rsa 私钥 + */ + private String rsaPrivateKey; + +} diff --git a/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/core/ApiDecryptParamResolver.java b/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/core/ApiDecryptParamResolver.java new file mode 100644 index 0000000..9dcf233 --- /dev/null +++ b/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/core/ApiDecryptParamResolver.java @@ -0,0 +1,76 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.api.crypto.core; + +import lombok.RequiredArgsConstructor; +import org.springblade.core.api.crypto.annotation.decrypt.ApiDecrypt; +import org.springblade.core.api.crypto.bean.CryptoInfoBean; +import org.springblade.core.api.crypto.config.ApiCryptoProperties; +import org.springblade.core.api.crypto.util.ApiCryptoUtil; +import org.springblade.core.tool.jackson.JsonUtil; +import org.springblade.core.tool.utils.Charsets; +import org.springblade.core.tool.utils.StringUtil; +import org.springframework.core.MethodParameter; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.lang.Nullable; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +import java.lang.reflect.Parameter; + +/** + * param 参数 解析 + * + * @author L.cm + */ +@RequiredArgsConstructor +public class ApiDecryptParamResolver implements HandlerMethodArgumentResolver { + private final ApiCryptoProperties properties; + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return AnnotatedElementUtils.hasAnnotation(parameter.getParameter(), ApiDecrypt.class); + } + + @Nullable + @Override + public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { + Parameter parameter = methodParameter.getParameter(); + ApiDecrypt apiDecrypt = AnnotatedElementUtils.getMergedAnnotation(parameter, ApiDecrypt.class); + String text = webRequest.getParameter(properties.getParamName()); + if (StringUtil.isBlank(text)) { + return null; + } + CryptoInfoBean infoBean = new CryptoInfoBean(apiDecrypt.value(), apiDecrypt.secretKey()); + byte[] textBytes = text.getBytes(Charsets.UTF_8); + byte[] decryptData = ApiCryptoUtil.decryptData(properties, textBytes, infoBean); + return JsonUtil.readValue(decryptData, parameter.getType()); + } +} diff --git a/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/core/ApiDecryptRequestBodyAdvice.java b/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/core/ApiDecryptRequestBodyAdvice.java new file mode 100644 index 0000000..28075c0 --- /dev/null +++ b/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/core/ApiDecryptRequestBodyAdvice.java @@ -0,0 +1,87 @@ +package org.springblade.core.api.crypto.core; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.api.crypto.annotation.decrypt.ApiDecrypt; +import org.springblade.core.api.crypto.bean.CryptoInfoBean; +import org.springblade.core.api.crypto.bean.DecryptHttpInputMessage; +import org.springblade.core.api.crypto.config.ApiCryptoProperties; +import org.springblade.core.api.crypto.exception.DecryptBodyFailException; +import org.springblade.core.api.crypto.util.ApiCryptoUtil; +import org.springblade.core.tool.utils.ClassUtil; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.core.MethodParameter; +import org.springframework.core.annotation.Order; +import org.springframework.http.HttpInputMessage; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.lang.NonNull; +import org.springframework.util.StreamUtils; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Type; + +/** + * 请求数据的加密信息解密处理
+ * 本类只对控制器参数中含有{@link org.springframework.web.bind.annotation.RequestBody} + * 以及package为org.springblade.core.api.signature.annotation.decrypt下的注解有效 + * + * @author licoy.cn, L.cm + * @see RequestBodyAdvice + */ +@Slf4j +@Order(1) +@AutoConfiguration +@ControllerAdvice +@RequiredArgsConstructor +@ConditionalOnProperty(value = ApiCryptoProperties.PREFIX + ".enabled", havingValue = "true", matchIfMissing = true) +public class ApiDecryptRequestBodyAdvice implements RequestBodyAdvice { + private final ApiCryptoProperties properties; + + @Override + public boolean supports(MethodParameter methodParameter, @NonNull Type targetType, @NonNull Class> converterType) { + return ClassUtil.isAnnotated(methodParameter.getMethod(), ApiDecrypt.class); + } + + @Override + public Object handleEmptyBody(Object body, @NonNull HttpInputMessage inputMessage, @NonNull MethodParameter parameter, + @NonNull Type targetType, @NonNull Class> converterType) { + return body; + } + + @NonNull + @Override + public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, @NonNull MethodParameter parameter, + @NonNull Type targetType, @NonNull Class> converterType) throws IOException { + // 判断 body 是否为空 + InputStream messageBody = inputMessage.getBody(); + if (messageBody.available() <= 0) { + return inputMessage; + } + byte[] decryptedBody = null; + CryptoInfoBean cryptoInfoBean = ApiCryptoUtil.getDecryptInfo(parameter); + if (cryptoInfoBean != null) { + // base64 byte array + byte[] bodyByteArray = StreamUtils.copyToByteArray(messageBody); + decryptedBody = ApiCryptoUtil.decryptData(properties, bodyByteArray, cryptoInfoBean); + } + if (decryptedBody == null) { + throw new DecryptBodyFailException("Decryption error, " + + "please check if the selected source data is encrypted correctly." + + " (解密错误,请检查选择的源数据的加密方式是否正确。)"); + } + InputStream inputStream = new ByteArrayInputStream(decryptedBody); + return new DecryptHttpInputMessage(inputStream, inputMessage.getHeaders()); + } + + @NonNull + @Override + public Object afterBodyRead(@NonNull Object body, @NonNull HttpInputMessage inputMessage, @NonNull MethodParameter parameter, @NonNull Type targetType, @NonNull Class> converterType) { + return body; + } + +} diff --git a/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/core/ApiEncryptResponseBodyAdvice.java b/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/core/ApiEncryptResponseBodyAdvice.java new file mode 100644 index 0000000..a491d54 --- /dev/null +++ b/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/core/ApiEncryptResponseBodyAdvice.java @@ -0,0 +1,64 @@ +package org.springblade.core.api.crypto.core; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.api.crypto.annotation.encrypt.ApiEncrypt; +import org.springblade.core.api.crypto.bean.CryptoInfoBean; +import org.springblade.core.api.crypto.config.ApiCryptoProperties; +import org.springblade.core.api.crypto.exception.EncryptBodyFailException; +import org.springblade.core.api.crypto.util.ApiCryptoUtil; +import org.springblade.core.tool.jackson.JsonUtil; +import org.springblade.core.tool.utils.ClassUtil; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.core.MethodParameter; +import org.springframework.core.annotation.Order; +import org.springframework.http.MediaType; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; + + +/** + * 响应数据的加密处理
+ * 本类只对控制器参数中含有{@link org.springframework.web.bind.annotation.ResponseBody} + * 或者控制类上含有{@link org.springframework.web.bind.annotation.RestController} + * 以及package为org.springblade.core.api.signature.annotation.encrypt下的注解有效 + * + * @author licoy.cn, L.cm + * @see ResponseBodyAdvice + */ +@Slf4j +@Order(1) +@AutoConfiguration +@ControllerAdvice +@RequiredArgsConstructor +@ConditionalOnProperty(value = ApiCryptoProperties.PREFIX + ".enabled", havingValue = "true", matchIfMissing = true) +public class ApiEncryptResponseBodyAdvice implements ResponseBodyAdvice { + private final ApiCryptoProperties properties; + + @Override + public boolean supports(MethodParameter returnType, @NonNull Class converterType) { + return ClassUtil.isAnnotated(returnType.getMethod(), ApiEncrypt.class); + } + + @Nullable + @Override + public Object beforeBodyWrite(Object body, @NonNull MethodParameter returnType, @NonNull MediaType selectedContentType, + @NonNull Class selectedConverterType, @NonNull ServerHttpRequest request, @NonNull ServerHttpResponse response) { + if (body == null) { + return null; + } + response.getHeaders().setContentType(MediaType.TEXT_PLAIN); + CryptoInfoBean cryptoInfoBean = ApiCryptoUtil.getEncryptInfo(returnType); + if (cryptoInfoBean != null) { + byte[] bodyJsonBytes = JsonUtil.toJsonAsBytes(body); + return ApiCryptoUtil.encryptData(properties, bodyJsonBytes, cryptoInfoBean); + } + throw new EncryptBodyFailException(); + } + +} diff --git a/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/enums/CryptoType.java b/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/enums/CryptoType.java new file mode 100644 index 0000000..1fe017d --- /dev/null +++ b/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/enums/CryptoType.java @@ -0,0 +1,25 @@ +package org.springblade.core.api.crypto.enums; + +/** + *

加密方式

+ * + * @author licoy.cn, L.cm + */ +public enum CryptoType { + + /** + * des + */ + DES, + + /** + * aes + */ + AES, + + /** + * rsa + */ + RSA + +} diff --git a/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/exception/DecryptBodyFailException.java b/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/exception/DecryptBodyFailException.java new file mode 100644 index 0000000..a103dbf --- /dev/null +++ b/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/exception/DecryptBodyFailException.java @@ -0,0 +1,13 @@ +package org.springblade.core.api.crypto.exception; + +/** + *

解密数据失败异常

+ * + * @author licoy.cn + */ +public class DecryptBodyFailException extends RuntimeException { + + public DecryptBodyFailException(String message) { + super(message); + } +} diff --git a/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/exception/EncryptBodyFailException.java b/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/exception/EncryptBodyFailException.java new file mode 100644 index 0000000..eb3bf70 --- /dev/null +++ b/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/exception/EncryptBodyFailException.java @@ -0,0 +1,17 @@ +package org.springblade.core.api.crypto.exception; + +/** + *

加密数据失败异常

+ * + * @author licoy.cn + */ +public class EncryptBodyFailException extends RuntimeException { + + public EncryptBodyFailException() { + super("Encrypted data failed. (加密数据失败)"); + } + + public EncryptBodyFailException(String message) { + super(message); + } +} diff --git a/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/exception/EncryptMethodNotFoundException.java b/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/exception/EncryptMethodNotFoundException.java new file mode 100644 index 0000000..f148fe8 --- /dev/null +++ b/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/exception/EncryptMethodNotFoundException.java @@ -0,0 +1,14 @@ +package org.springblade.core.api.crypto.exception; + +/** + *

加密方式未找到或未定义异常

+ * + * @author licoy.cn + */ +public class EncryptMethodNotFoundException extends RuntimeException { + + public EncryptMethodNotFoundException() { + super("Encryption method is not defined. (加密方式未定义)"); + } + +} diff --git a/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/exception/KeyNotConfiguredException.java b/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/exception/KeyNotConfiguredException.java new file mode 100644 index 0000000..4fc05f5 --- /dev/null +++ b/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/exception/KeyNotConfiguredException.java @@ -0,0 +1,14 @@ +package org.springblade.core.api.crypto.exception; + + +/** + *

未配置KEY运行时异常

+ * + * @author licoy.cn, L.cm + */ +public class KeyNotConfiguredException extends RuntimeException { + + public KeyNotConfiguredException(String message) { + super(message); + } +} diff --git a/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/util/ApiCryptoUtil.java b/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/util/ApiCryptoUtil.java new file mode 100644 index 0000000..fa66b67 --- /dev/null +++ b/blade-starter-api-crypto/src/main/java/org/springblade/core/api/crypto/util/ApiCryptoUtil.java @@ -0,0 +1,124 @@ +package org.springblade.core.api.crypto.util; + +import org.springblade.core.api.crypto.annotation.decrypt.ApiDecrypt; +import org.springblade.core.api.crypto.annotation.encrypt.ApiEncrypt; +import org.springblade.core.api.crypto.bean.CryptoInfoBean; +import org.springblade.core.api.crypto.config.ApiCryptoProperties; +import org.springblade.core.api.crypto.enums.CryptoType; +import org.springblade.core.api.crypto.exception.EncryptBodyFailException; +import org.springblade.core.api.crypto.exception.EncryptMethodNotFoundException; +import org.springblade.core.api.crypto.exception.KeyNotConfiguredException; +import org.springblade.core.tool.utils.*; +import org.springframework.core.MethodParameter; +import org.springframework.lang.Nullable; + +import java.util.Objects; + +/** + *

辅助检测工具类

+ * + * @author licoy.cn, L.cm + */ +public class ApiCryptoUtil { + + /** + * 获取方法控制器上的加密注解信息 + * + * @param methodParameter 控制器方法 + * @return 加密注解信息 + */ + @Nullable + public static CryptoInfoBean getEncryptInfo(MethodParameter methodParameter) { + ApiEncrypt encryptBody = ClassUtil.getAnnotation(methodParameter.getMethod(), ApiEncrypt.class); + if (encryptBody == null) { + return null; + } + return new CryptoInfoBean(encryptBody.value(), encryptBody.secretKey()); + } + + /** + * 获取方法控制器上的解密注解信息 + * + * @param methodParameter 控制器方法 + * @return 加密注解信息 + */ + @Nullable + public static CryptoInfoBean getDecryptInfo(MethodParameter methodParameter) { + ApiDecrypt decryptBody = ClassUtil.getAnnotation(methodParameter.getMethod(), ApiDecrypt.class); + if (decryptBody == null) { + return null; + } + return new CryptoInfoBean(decryptBody.value(), decryptBody.secretKey()); + } + + /** + * 选择加密方式并进行加密 + * + * @param jsonData json 数据 + * @param infoBean 加密信息 + * @return 加密结果 + */ + public static String encryptData(ApiCryptoProperties properties, byte[] jsonData, CryptoInfoBean infoBean) { + CryptoType type = infoBean.getType(); + if (type == null) { + throw new EncryptMethodNotFoundException(); + } + String secretKey = infoBean.getSecretKey(); + if (type == CryptoType.DES) { + secretKey = ApiCryptoUtil.checkSecretKey(properties.getDesKey(), secretKey, "DES"); + return DesUtil.encryptToBase64(jsonData, secretKey); + } + if (type == CryptoType.AES) { + secretKey = ApiCryptoUtil.checkSecretKey(properties.getAesKey(), secretKey, "AES"); + return AesUtil.encryptToBase64(jsonData, secretKey); + } + if (type == CryptoType.RSA) { + String privateKey = Objects.requireNonNull(properties.getRsaPrivateKey()); + return RsaUtil.encryptByPrivateKeyToBase64(privateKey, jsonData); + } + throw new EncryptBodyFailException(); + } + + /** + * 选择加密方式并进行解密 + * + * @param bodyData byte array + * @param infoBean 加密信息 + * @return 解密结果 + */ + public static byte[] decryptData(ApiCryptoProperties properties, byte[] bodyData, CryptoInfoBean infoBean) { + CryptoType type = infoBean.getType(); + if (type == null) { + throw new EncryptMethodNotFoundException(); + } + String secretKey = infoBean.getSecretKey(); + if (type == CryptoType.AES) { + secretKey = ApiCryptoUtil.checkSecretKey(properties.getAesKey(), secretKey, "AES"); + return AesUtil.decryptFormBase64(bodyData, secretKey); + } + if (type == CryptoType.DES) { + secretKey = ApiCryptoUtil.checkSecretKey(properties.getDesKey(), secretKey, "DES"); + return DesUtil.decryptFormBase64(bodyData, secretKey); + } + if (type == CryptoType.RSA) { + String privateKey = Objects.requireNonNull(properties.getRsaPrivateKey()); + return RsaUtil.decryptFromBase64(privateKey, bodyData); + } + throw new EncryptMethodNotFoundException(); + } + + /** + * 检验私钥 + * + * @param k1 配置的私钥 + * @param k2 注解上的私钥 + * @param keyName key名称 + * @return 私钥 + */ + private static String checkSecretKey(String k1, String k2, String keyName) { + if (StringUtil.isBlank(k1) && StringUtil.isBlank(k2)) { + throw new KeyNotConfiguredException(String.format("%s key is not configured (未配置%s)", keyName, keyName)); + } + return StringUtil.isBlank(k2) ? k1 : k2; + } +} diff --git a/blade-starter-auth/pom.xml b/blade-starter-auth/pom.xml new file mode 100644 index 0000000..8c03eb7 --- /dev/null +++ b/blade-starter-auth/pom.xml @@ -0,0 +1,29 @@ + + + 4.0.0 + + BladeX-Tool + org.springblade + ${revision} + + + blade-starter-auth + ${project.artifactId} + ${project.parent.version} + jar + + + + + org.springblade + blade-core-tool + + + org.springblade + blade-starter-jwt + + + + diff --git a/blade-starter-auth/src/main/java/org/springblade/core/secure/AuthInfo.java b/blade-starter-auth/src/main/java/org/springblade/core/secure/AuthInfo.java new file mode 100644 index 0000000..87a40fb --- /dev/null +++ b/blade-starter-auth/src/main/java/org/springblade/core/secure/AuthInfo.java @@ -0,0 +1,55 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.secure; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * AuthInfo + * + * @author Chill + */ +@Data +@Schema(description = "认证信息") +public class AuthInfo { + @Schema(description = "令牌") + private String accessToken; + @Schema(description = "令牌类型") + private String tokenType; + @Schema(description = "头像") + private String avatar = "https://bladex.cn/images/logo.png"; + @Schema(description = "角色名") + private String authority; + @Schema(description = "用户名") + private String userName; + @Schema(description = "账号名") + private String account; + @Schema(description = "过期时间") + private long expiresIn; + @Schema(description = "许可证") + private String license = "powered by bladex"; +} diff --git a/blade-starter-auth/src/main/java/org/springblade/core/secure/BladeUser.java b/blade-starter-auth/src/main/java/org/springblade/core/secure/BladeUser.java new file mode 100644 index 0000000..af47d9f --- /dev/null +++ b/blade-starter-auth/src/main/java/org/springblade/core/secure/BladeUser.java @@ -0,0 +1,109 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.secure; + +import io.swagger.v3.oas.annotations.Hidden; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springblade.core.tool.support.Kv; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 用户实体 + * + * @author Chill + */ +@Data +@Hidden +public class BladeUser implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + /** + * 客户端id + */ + @Schema(hidden = true) + private String clientId; + + /** + * 用户id + */ + @Schema(hidden = true) + private Long userId; + /** + * 账号 + */ + @Schema(hidden = true) + private String account; + /** + * 用户名 + */ + @Schema(hidden = true) + private String userName; + /** + * 昵称 + */ + @Schema(hidden = true) + private String nickName; + /** + * 租户ID + */ + @Schema(hidden = true) + private String tenantId; + /** + * 第三方认证ID + */ + @Schema(hidden = true) + private String oauthId; + /** + * 部门id + */ + @Schema(hidden = true) + private String deptId; + /** + * 岗位id + */ + @Schema(hidden = true) + private String postId; + /** + * 角色id + */ + @Schema(hidden = true) + private String roleId; + /** + * 角色名 + */ + @Schema(hidden = true) + private String roleName; + /** + * 用户详情 + */ + @Schema(hidden = true) + private Kv detail; + +} diff --git a/blade-starter-auth/src/main/java/org/springblade/core/secure/TokenInfo.java b/blade-starter-auth/src/main/java/org/springblade/core/secure/TokenInfo.java new file mode 100644 index 0000000..15ba573 --- /dev/null +++ b/blade-starter-auth/src/main/java/org/springblade/core/secure/TokenInfo.java @@ -0,0 +1,48 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.secure; + +import lombok.Data; + +/** + * TokenInfo + * + * @author Chill + */ +@Data +public class TokenInfo { + + /** + * 令牌值 + */ + private String token; + + /** + * 过期秒数 + */ + private int expire; + +} diff --git a/blade-starter-auth/src/main/java/org/springblade/core/secure/exception/SecureException.java b/blade-starter-auth/src/main/java/org/springblade/core/secure/exception/SecureException.java new file mode 100644 index 0000000..31e56c1 --- /dev/null +++ b/blade-starter-auth/src/main/java/org/springblade/core/secure/exception/SecureException.java @@ -0,0 +1,65 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.secure.exception; + +import lombok.Getter; +import org.springblade.core.tool.api.IResultCode; +import org.springblade.core.tool.api.ResultCode; + +import java.io.Serial; + +/** + * Secure异常 + * + * @author Chill + */ +public class SecureException extends RuntimeException { + @Serial + private static final long serialVersionUID = 1L; + + @Getter + private final IResultCode resultCode; + + public SecureException(String message) { + super(message); + this.resultCode = ResultCode.UN_AUTHORIZED; + } + + public SecureException(IResultCode resultCode) { + super(resultCode.getMessage()); + this.resultCode = resultCode; + } + + public SecureException(IResultCode resultCode, Throwable cause) { + super(cause); + this.resultCode = resultCode; + } + + @Override + public Throwable fillInStackTrace() { + return this; + } +} diff --git a/blade-starter-auth/src/main/java/org/springblade/core/secure/utils/AuthUtil.java b/blade-starter-auth/src/main/java/org/springblade/core/secure/utils/AuthUtil.java new file mode 100644 index 0000000..9252d96 --- /dev/null +++ b/blade-starter-auth/src/main/java/org/springblade/core/secure/utils/AuthUtil.java @@ -0,0 +1,518 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.secure.utils; + +import io.jsonwebtoken.Claims; +import org.springblade.core.jwt.JwtCrypto; +import org.springblade.core.jwt.JwtUtil; +import org.springblade.core.jwt.props.JwtProperties; +import org.springblade.core.launch.constant.TokenConstant; +import org.springblade.core.launch.props.BladeProperties; +import org.springblade.core.secure.BladeUser; +import org.springblade.core.tool.constant.RoleConstant; +import org.springblade.core.tool.support.Kv; +import org.springblade.core.tool.utils.*; + +import jakarta.servlet.http.HttpServletRequest; + +import java.util.Map; +import java.util.Objects; + +import static org.springblade.core.jwt.JwtCrypto.BLADE_TOKEN_CRYPTO_KEY; + +/** + * Auth工具类 + * + * @author Chill + */ +public class AuthUtil { + private static final String BLADE_USER_REQUEST_ATTR = "_BLADE_USER_REQUEST_ATTR_"; + private final static String BLADE_SECURE_HEADER_KEY = "Blade-Requested-With"; + private final static String BLADE_SECURE_HEADER_VALUE = "BladeHttpRequest"; + private final static String HEADER = TokenConstant.HEADER; + private final static String ACCOUNT = TokenConstant.ACCOUNT; + private final static String USER_NAME = TokenConstant.USER_NAME; + private final static String NICK_NAME = TokenConstant.NICK_NAME; + private final static String USER_ID = TokenConstant.USER_ID; + private final static String DEPT_ID = TokenConstant.DEPT_ID; + private final static String POST_ID = TokenConstant.POST_ID; + private final static String ROLE_ID = TokenConstant.ROLE_ID; + private final static String ROLE_NAME = TokenConstant.ROLE_NAME; + private final static String TENANT_ID = TokenConstant.TENANT_ID; + private final static String OAUTH_ID = TokenConstant.OAUTH_ID; + private final static String CLIENT_ID = TokenConstant.CLIENT_ID; + private final static String DETAIL = TokenConstant.DETAIL; + + private static BladeProperties bladeProperties; + private static JwtProperties jwtProperties; + + /** + * 获取配置类 + * + * @return jwtProperties + */ + private static BladeProperties getBladeProperties() { + if (bladeProperties == null) { + bladeProperties = SpringUtil.getBean(BladeProperties.class); + } + return bladeProperties; + } + + private static JwtProperties getJwtProperties() { + if (jwtProperties == null) { + jwtProperties = SpringUtil.getBean(JwtProperties.class); + } + return jwtProperties; + } + + /** + * 获取用户信息 + * + * @return BladeUser + */ + public static BladeUser getUser() { + HttpServletRequest request = WebUtil.getRequest(); + if (request == null) { + return null; + } + // 优先从 request 中获取 + Object bladeUser = request.getAttribute(BLADE_USER_REQUEST_ATTR); + if (bladeUser == null) { + bladeUser = getUser(request); + if (bladeUser != null) { + // 设置到 request 中 + request.setAttribute(BLADE_USER_REQUEST_ATTR, bladeUser); + } + } + return (BladeUser) bladeUser; + } + + /** + * 获取用户信息 + * + * @param request request + * @return BladeUser + */ + @SuppressWarnings("unchecked") + public static BladeUser getUser(HttpServletRequest request) { + Claims claims = getClaims(request); + if (claims == null) { + return null; + } + String clientId = Func.toStr(claims.get(AuthUtil.CLIENT_ID)); + Long userId = Func.toLong(claims.get(AuthUtil.USER_ID)); + String tenantId = Func.toStr(claims.get(AuthUtil.TENANT_ID)); + String oauthId = Func.toStr(claims.get(AuthUtil.OAUTH_ID)); + String deptId = Func.toStrWithEmpty(claims.get(AuthUtil.DEPT_ID), StringPool.MINUS_ONE); + String postId = Func.toStrWithEmpty(claims.get(AuthUtil.POST_ID), StringPool.MINUS_ONE); + String roleId = Func.toStrWithEmpty(claims.get(AuthUtil.ROLE_ID), StringPool.MINUS_ONE); + String account = Func.toStr(claims.get(AuthUtil.ACCOUNT)); + String roleName = Func.toStr(claims.get(AuthUtil.ROLE_NAME)); + String userName = Func.toStr(claims.get(AuthUtil.USER_NAME)); + String nickName = Func.toStr(claims.get(AuthUtil.NICK_NAME)); + Kv detail = Kv.create().setAll((Map) claims.get(AuthUtil.DETAIL)); + BladeUser bladeUser = new BladeUser(); + bladeUser.setClientId(clientId); + bladeUser.setUserId(userId); + bladeUser.setTenantId(tenantId); + bladeUser.setOauthId(oauthId); + bladeUser.setAccount(account); + bladeUser.setDeptId(deptId); + bladeUser.setPostId(postId); + bladeUser.setRoleId(roleId); + bladeUser.setRoleName(roleName); + bladeUser.setUserName(userName); + bladeUser.setNickName(nickName); + bladeUser.setDetail(detail); + return bladeUser; + } + + /** + * 是否为超管 + * + * @return boolean + */ + public static boolean isAdministrator() { + return StringUtil.containsAny(getUserRole(), RoleConstant.ADMINISTRATOR); + } + + /** + * 是否为管理员 + * + * @return boolean + */ + public static boolean isAdmin() { + return StringUtil.containsAny(getUserRole(), RoleConstant.ADMIN); + } + + /** + * 获取用户id + * + * @return userId + */ + public static Long getUserId() { + BladeUser user = getUser(); + return (null == user) ? -1 : user.getUserId(); + } + + /** + * 获取用户id + * + * @param request request + * @return userId + */ + public static Long getUserId(HttpServletRequest request) { + BladeUser user = getUser(request); + return (null == user) ? -1 : user.getUserId(); + } + + /** + * 获取用户账号 + * + * @return userAccount + */ + public static String getUserAccount() { + BladeUser user = getUser(); + return (null == user) ? StringPool.EMPTY : user.getAccount(); + } + + /** + * 获取用户账号 + * + * @param request request + * @return userAccount + */ + public static String getUserAccount(HttpServletRequest request) { + BladeUser user = getUser(request); + return (null == user) ? StringPool.EMPTY : user.getAccount(); + } + + /** + * 获取用户名 + * + * @return userName + */ + public static String getUserName() { + BladeUser user = getUser(); + return (null == user) ? StringPool.EMPTY : user.getUserName(); + } + + /** + * 获取用户名 + * + * @param request request + * @return userName + */ + public static String getUserName(HttpServletRequest request) { + BladeUser user = getUser(request); + return (null == user) ? StringPool.EMPTY : user.getUserName(); + } + + /** + * 获取昵称 + * + * @return userName + */ + public static String getNickName() { + BladeUser user = getUser(); + return (null == user) ? StringPool.EMPTY : user.getNickName(); + } + + /** + * 获取昵称 + * + * @param request request + * @return userName + */ + public static String getNickName(HttpServletRequest request) { + BladeUser user = getUser(request); + return (null == user) ? StringPool.EMPTY : user.getNickName(); + } + + /** + * 获取用户部门 + * + * @return userName + */ + public static String getDeptId() { + BladeUser user = getUser(); + return (null == user) ? StringPool.EMPTY : user.getDeptId(); + } + + /** + * 获取用户部门 + * + * @param request request + * @return userName + */ + public static String getDeptId(HttpServletRequest request) { + BladeUser user = getUser(request); + return (null == user) ? StringPool.EMPTY : user.getDeptId(); + } + + /** + * 获取用户岗位 + * + * @return userName + */ + public static String getPostId() { + BladeUser user = getUser(); + return (null == user) ? StringPool.EMPTY : user.getPostId(); + } + + /** + * 获取用户岗位 + * + * @param request request + * @return userName + */ + public static String getPostId(HttpServletRequest request) { + BladeUser user = getUser(request); + return (null == user) ? StringPool.EMPTY : user.getPostId(); + } + + /** + * 获取用户角色 + * + * @return userName + */ + public static String getUserRole() { + BladeUser user = getUser(); + return (null == user) ? StringPool.EMPTY : user.getRoleName(); + } + + /** + * 获取用角色 + * + * @param request request + * @return userName + */ + public static String getUserRole(HttpServletRequest request) { + BladeUser user = getUser(request); + return (null == user) ? StringPool.EMPTY : user.getRoleName(); + } + + /** + * 获取租户ID + * + * @return tenantId + */ + public static String getTenantId() { + BladeUser user = getUser(); + return (null == user) ? StringPool.EMPTY : user.getTenantId(); + } + + /** + * 获取租户ID + * + * @param request request + * @return tenantId + */ + public static String getTenantId(HttpServletRequest request) { + BladeUser user = getUser(request); + return (null == user) ? StringPool.EMPTY : user.getTenantId(); + } + + /** + * 获取第三方认证ID + * + * @return tenantId + */ + public static String getOauthId() { + BladeUser user = getUser(); + return (null == user) ? StringPool.EMPTY : user.getOauthId(); + } + + /** + * 获取第三方认证ID + * + * @param request request + * @return tenantId + */ + public static String getOauthId(HttpServletRequest request) { + BladeUser user = getUser(request); + return (null == user) ? StringPool.EMPTY : user.getOauthId(); + } + + /** + * 获取客户端id + * + * @return clientId + */ + public static String getClientId() { + BladeUser user = getUser(); + return (null == user) ? StringPool.EMPTY : user.getClientId(); + } + + /** + * 获取客户端id + * + * @param request request + * @return clientId + */ + public static String getClientId(HttpServletRequest request) { + BladeUser user = getUser(request); + return (null == user) ? StringPool.EMPTY : user.getClientId(); + } + + /** + * 获取用户详情 + * + * @return clientId + */ + public static Kv getDetail() { + BladeUser user = getUser(); + return (null == user) ? Kv.create() : user.getDetail(); + } + + /** + * 获取用户详情 + * + * @param request request + * @return clientId + */ + public static Kv getDetail(HttpServletRequest request) { + BladeUser user = getUser(request); + return (null == user) ? Kv.create() : user.getDetail(); + } + + /** + * 用户信息是否缺失 + * + * @return boolean + */ + public static boolean userIncomplete() { + return userIncomplete(Objects.requireNonNull(getUser())); + } + + /** + * 用户信息是否缺失 + * + * @param user user + * @return boolean + */ + public static boolean userIncomplete(BladeUser user) { + if (Func.isEmpty(user)) { + return true; + } + if (Func.hasEmpty(user.getUserId(), + user.getAccount(), + user.getTenantId(), + user.getClientId(), + user.getDeptId(), + user.getRoleId())) { + return true; + } + return NumberUtil.toLong(String.valueOf(user.getUserId()), -1L) < 0 + || NumberUtil.toLong(user.getDeptId(), -1L) < 0 + || NumberUtil.toLong(user.getRoleId(), -1L) < 0; + } + + /** + * 不包含安全请求头 + * + * @return boolean + */ + public static boolean secureHeaderIncomplete() { + HttpServletRequest request = WebUtil.getRequest(); + String value = Objects.requireNonNull(request).getHeader(BLADE_SECURE_HEADER_KEY); + return !StringUtil.equals(BLADE_SECURE_HEADER_VALUE, value); + } + + /** + * 获取Claims + * + * @param request request + * @return Claims + */ + public static Claims getClaims(HttpServletRequest request) { + // 获取 Token 参数 + String auth = request.getHeader(AuthUtil.HEADER); + String token = getToken( + StringUtil.isNotBlank(auth) ? auth : request.getParameter(AuthUtil.HEADER) + ); + // 获取 Token 值 + Claims claims = null; + if (StringUtil.isNotBlank(token)) { + claims = AuthUtil.parseJWT(token); + } + // 判断 Token 状态 + if (ObjectUtil.isNotEmpty(claims) && getJwtProperties().getState()) { + String tenantId = Func.toStr(claims.get(AuthUtil.TENANT_ID)); + String clientId = Func.toStr(claims.get(AuthUtil.CLIENT_ID)); + String userId = Func.toStr(claims.get(AuthUtil.USER_ID)); + String accessToken = JwtUtil.getAccessToken(tenantId, clientId, userId, token); + if (!token.equalsIgnoreCase(accessToken)) { + return null; + } + } + return claims; + } + + /** + * 获取请求头 + * + * @return header + */ + public static String getHeader() { + return getHeader(Objects.requireNonNull(WebUtil.getRequest())); + } + + /** + * 获取请求头 + * + * @param request request + * @return header + */ + public static String getHeader(HttpServletRequest request) { + return request.getHeader(HEADER); + } + + /** + * 解析jsonWebToken + * + * @param jsonWebToken jsonWebToken + * @return Claims + */ + public static Claims parseJWT(String jsonWebToken) { + return JwtUtil.parseJWT(jsonWebToken); + } + + /** + * 获取Token + * + * @param auth 认证串 + * @return string + */ + public static String getToken(String auth) { + String token = JwtUtil.getToken(auth); + if (JwtUtil.isCrypto(auth)) { + token = JwtCrypto.decryptToString(token, getBladeProperties().getEnvironment().getProperty(BLADE_TOKEN_CRYPTO_KEY)); + } + return token; + } + +} diff --git a/blade-starter-cache/pom.xml b/blade-starter-cache/pom.xml new file mode 100644 index 0000000..a596d55 --- /dev/null +++ b/blade-starter-cache/pom.xml @@ -0,0 +1,40 @@ + + + 4.0.0 + + BladeX-Tool + org.springblade + ${revision} + + + blade-starter-cache + ${project.artifactId} + ${project.parent.version} + jar + + + + + org.springblade + blade-core-tool + + + org.springblade + blade-starter-auth + + + + org.springframework.boot + spring-boot-starter-cache + + + + org.springblade + blade-core-auto + provided + + + + diff --git a/blade-starter-cache/src/main/java/org/springblade/core/cache/config/CacheConfiguration.java b/blade-starter-cache/src/main/java/org/springblade/core/cache/config/CacheConfiguration.java new file mode 100644 index 0000000..c292cd2 --- /dev/null +++ b/blade-starter-cache/src/main/java/org/springblade/core/cache/config/CacheConfiguration.java @@ -0,0 +1,39 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.cache.config; + +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.cache.annotation.EnableCaching; + +/** + * Cache配置类 + * + * @author Chill + */ +@EnableCaching +@AutoConfiguration +public class CacheConfiguration { +} diff --git a/blade-starter-cache/src/main/java/org/springblade/core/cache/constant/CacheConstant.java b/blade-starter-cache/src/main/java/org/springblade/core/cache/constant/CacheConstant.java new file mode 100644 index 0000000..1873c7d --- /dev/null +++ b/blade-starter-cache/src/main/java/org/springblade/core/cache/constant/CacheConstant.java @@ -0,0 +1,61 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.cache.constant; + +/** + * 缓存名 + * + * @author Chill + */ +public interface CacheConstant { + + String BIZ_CACHE = "blade:biz"; + + String MENU_CACHE = "blade:menu"; + + String USER_CACHE = "blade:user"; + + String DICT_CACHE = "blade:dict"; + + String FLOW_CACHE = "blade:flow"; + + String SYS_CACHE = "blade:sys"; + + String RESOURCE_CACHE = "blade:resource"; + + String PARAM_CACHE = "blade:param"; + + String DEFAULT_CACHE = "default:cache"; + + String RETRY_LIMIT_CACHE = "retry:limit:cache"; + + String HALF_HOUR = "half:hour"; + + String HOUR = "hour"; + + String ONE_DAY = "one:day"; + +} diff --git a/blade-starter-cache/src/main/java/org/springblade/core/cache/utils/CacheUtil.java b/blade-starter-cache/src/main/java/org/springblade/core/cache/utils/CacheUtil.java new file mode 100644 index 0000000..24a98c1 --- /dev/null +++ b/blade-starter-cache/src/main/java/org/springblade/core/cache/utils/CacheUtil.java @@ -0,0 +1,340 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.cache.utils; + +import org.springblade.core.secure.utils.AuthUtil; +import org.springblade.core.tool.constant.BladeConstant; +import org.springblade.core.tool.utils.*; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.lang.Nullable; + +import java.lang.reflect.Field; +import java.util.List; +import java.util.concurrent.Callable; + +/** + * 缓存工具类 + * + * @author Chill + */ +public class CacheUtil { + + private static CacheManager cacheManager; + + private static final Boolean TENANT_MODE = Boolean.TRUE; + + /** + * 获取缓存工具 + * + * @return CacheManager + */ + private static CacheManager getCacheManager() { + if (cacheManager == null) { + cacheManager = SpringUtil.getBean(CacheManager.class); + } + return cacheManager; + } + + /** + * 获取缓存对象 + * + * @param cacheName 缓存名 + * @return Cache + */ + public static Cache getCache(String cacheName) { + return getCache(cacheName, TENANT_MODE); + } + + /** + * 获取缓存对象 + * + * @param cacheName 缓存名 + * @param tenantMode 租户模式 + * @return Cache + */ + public static Cache getCache(String cacheName, Boolean tenantMode) { + return getCacheManager().getCache(formatCacheName(cacheName, tenantMode)); + } + + /** + * 获取缓存对象 + * + * @param cacheName 缓存名 + * @param tenantId 租户ID + * @return Cache + */ + public static Cache getCache(String cacheName, String tenantId) { + return getCacheManager().getCache(formatCacheName(cacheName, tenantId)); + } + + /** + * 根据租户信息格式化缓存名 + * + * @param cacheName 缓存名 + * @param tenantMode 租户模式 + * @return String + */ + public static String formatCacheName(String cacheName, Boolean tenantMode) { + if (!tenantMode) { + return cacheName; + } + return formatCacheName(cacheName, AuthUtil.getTenantId()); + } + + /** + * 根据租户信息格式化缓存名 + * + * @param cacheName 缓存名 + * @param tenantId 租户ID + * @return String + */ + public static String formatCacheName(String cacheName, String tenantId) { + return (StringUtil.isBlank(tenantId) ? cacheName : tenantId.concat(StringPool.COLON).concat(cacheName)); + } + + /** + * 获取缓存 + * + * @param cacheName 缓存名 + * @param keyPrefix 缓存键前缀 + * @param key 缓存键值 + * @return Object + */ + @Nullable + public static Object get(String cacheName, String keyPrefix, Object key) { + return get(cacheName, keyPrefix, key, TENANT_MODE); + } + + /** + * 获取缓存 + * + * @param cacheName 缓存名 + * @param keyPrefix 缓存键前缀 + * @param key 缓存键值 + * @param tenantMode 租户模式 + * @return Object + */ + @Nullable + public static Object get(String cacheName, String keyPrefix, Object key, Boolean tenantMode) { + if (Func.hasEmpty(cacheName, keyPrefix, key)) { + return null; + } + Cache.ValueWrapper valueWrapper = getCache(cacheName, tenantMode).get(keyPrefix.concat(String.valueOf(key))); + if (Func.isEmpty(valueWrapper)) { + return null; + } + return valueWrapper.get(); + } + + /** + * 获取缓存 + * + * @param cacheName 缓存名 + * @param keyPrefix 缓存键前缀 + * @param key 缓存键值 + * @param type 类型 + * @param 泛型 + * @return T + */ + @Nullable + public static T get(String cacheName, String keyPrefix, Object key, @Nullable Class type) { + return get(cacheName, keyPrefix, key, type, TENANT_MODE); + } + + /** + * 获取缓存 + * + * @param cacheName 缓存名 + * @param keyPrefix 缓存键前缀 + * @param key 缓存键值 + * @param type 类型 + * @param tenantMode 租户模式 + * @param 泛型 + * @return T + */ + @Nullable + public static T get(String cacheName, String keyPrefix, Object key, @Nullable Class type, Boolean tenantMode) { + if (Func.hasEmpty(cacheName, keyPrefix, key)) { + return null; + } + return getCache(cacheName, tenantMode).get(keyPrefix.concat(String.valueOf(key)), type); + } + + /** + * 获取缓存 + * + * @param cacheName 缓存名 + * @param keyPrefix 缓存键前缀 + * @param key 缓存键值 + * @param valueLoader 重载对象 + * @param 泛型 + * @return T + */ + @Nullable + public static T get(String cacheName, String keyPrefix, Object key, Callable valueLoader) { + return get(cacheName, keyPrefix, key, valueLoader, TENANT_MODE); + } + + /** + * 获取缓存 + * + * @param cacheName 缓存名 + * @param keyPrefix 缓存键前缀 + * @param key 缓存键值 + * @param valueLoader 重载对象 + * @param tenantMode 租户模式 + * @param 泛型 + * @return T + */ + @Nullable + public static T get(String cacheName, String keyPrefix, Object key, Callable valueLoader, Boolean tenantMode) { + if (Func.hasEmpty(cacheName, keyPrefix, key)) { + return null; + } + try { + Cache.ValueWrapper valueWrapper = getCache(cacheName, tenantMode).get(keyPrefix.concat(String.valueOf(key))); + Object value = null; + if (valueWrapper == null) { + T call = valueLoader.call(); + if (ObjectUtil.isNotEmpty(call)) { + Field field = ReflectUtil.getField(call.getClass(), BladeConstant.DB_PRIMARY_KEY); + if (ObjectUtil.isNotEmpty(field) && ObjectUtil.isEmpty(ClassUtil.getMethod(call.getClass(), BladeConstant.DB_PRIMARY_KEY_METHOD).invoke(call))) { + return null; + } + getCache(cacheName, tenantMode).put(keyPrefix.concat(String.valueOf(key)), call); + value = call; + } + } else { + value = valueWrapper.get(); + } + return (T) value; + } catch (Exception ex) { + ex.printStackTrace(); + return null; + } + } + + /** + * 设置缓存 + * + * @param cacheName 缓存名 + * @param keyPrefix 缓存键前缀 + * @param key 缓存键值 + * @param value 缓存值 + */ + public static void put(String cacheName, String keyPrefix, Object key, @Nullable Object value) { + put(cacheName, keyPrefix, key, value, TENANT_MODE); + } + + /** + * 设置缓存 + * + * @param cacheName 缓存名 + * @param keyPrefix 缓存键前缀 + * @param key 缓存键值 + * @param value 缓存值 + * @param tenantMode 租户模式 + */ + public static void put(String cacheName, String keyPrefix, Object key, @Nullable Object value, Boolean tenantMode) { + getCache(cacheName, tenantMode).put(keyPrefix.concat(String.valueOf(key)), value); + } + + /** + * 清除缓存 + * + * @param cacheName 缓存名 + * @param keyPrefix 缓存键前缀 + * @param key 缓存键值 + */ + public static void evict(String cacheName, String keyPrefix, Object key) { + evict(cacheName, keyPrefix, key, TENANT_MODE); + } + + /** + * 清除缓存 + * + * @param cacheName 缓存名 + * @param keyPrefix 缓存键前缀 + * @param key 缓存键值 + * @param tenantMode 租户模式 + */ + public static void evict(String cacheName, String keyPrefix, Object key, Boolean tenantMode) { + if (Func.hasEmpty(cacheName, keyPrefix, key)) { + return; + } + getCache(cacheName, tenantMode).evict(keyPrefix.concat(String.valueOf(key))); + } + + /** + * 清空缓存 + * + * @param cacheName 缓存名 + */ + public static void clear(String cacheName) { + clear(cacheName, TENANT_MODE); + } + + /** + * 清空缓存 + * + * @param cacheName 缓存名 + * @param tenantMode 租户模式 + */ + public static void clear(String cacheName, Boolean tenantMode) { + if (Func.isEmpty(cacheName)) { + return; + } + getCache(cacheName, tenantMode).clear(); + } + + /** + * 清空缓存 + * + * @param cacheName 缓存名 + * @param tenantId 租户ID + */ + public static void clear(String cacheName, String tenantId) { + if (Func.isEmpty(cacheName)) { + return; + } + getCache(cacheName, tenantId).clear(); + } + + /** + * 清空缓存 + * + * @param cacheName 缓存名 + * @param tenantIds 租户ID集合 + */ + public static void clear(String cacheName, List tenantIds) { + if (Func.isEmpty(cacheName)) { + return; + } + tenantIds.forEach(tenantId -> getCache(cacheName, tenantId).clear()); + } + +} diff --git a/blade-starter-datascope/pom.xml b/blade-starter-datascope/pom.xml new file mode 100644 index 0000000..d4cab40 --- /dev/null +++ b/blade-starter-datascope/pom.xml @@ -0,0 +1,33 @@ + + + 4.0.0 + + BladeX-Tool + org.springblade + ${revision} + + + blade-starter-datascope + ${project.artifactId} + ${project.parent.version} + jar + + + + org.springblade + blade-starter-cache + + + org.springblade + blade-starter-mybatis + + + org.springblade + blade-core-auto + provided + + + + diff --git a/blade-starter-datascope/src/main/java/org/springblade/core/datascope/annotation/DataAuth.java b/blade-starter-datascope/src/main/java/org/springblade/core/datascope/annotation/DataAuth.java new file mode 100644 index 0000000..d4851e6 --- /dev/null +++ b/blade-starter-datascope/src/main/java/org/springblade/core/datascope/annotation/DataAuth.java @@ -0,0 +1,70 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.datascope.annotation; + +import org.springblade.core.datascope.constant.DataScopeConstant; +import org.springblade.core.datascope.enums.DataScopeEnum; + +import java.lang.annotation.*; + +/** + * 数据权限定义 + * + * @author Chill + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +public @interface DataAuth { + + /** + * 资源编号 + */ + String code() default ""; + + /** + * 数据权限对应字段 + */ + String column() default DataScopeConstant.DEFAULT_COLUMN; + + /** + * 数据权限规则 + */ + DataScopeEnum type() default DataScopeEnum.ALL; + + /** + * 可见字段 + */ + String field() default "*"; + + /** + * 数据权限规则值域 + */ + String value() default ""; + +} + diff --git a/blade-starter-datascope/src/main/java/org/springblade/core/datascope/config/DataScopeConfiguration.java b/blade-starter-datascope/src/main/java/org/springblade/core/datascope/config/DataScopeConfiguration.java new file mode 100644 index 0000000..b1855a9 --- /dev/null +++ b/blade-starter-datascope/src/main/java/org/springblade/core/datascope/config/DataScopeConfiguration.java @@ -0,0 +1,74 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.datascope.config; + +import lombok.AllArgsConstructor; +import org.springblade.core.datascope.handler.BladeDataScopeHandler; +import org.springblade.core.datascope.handler.BladeScopeModelHandler; +import org.springblade.core.datascope.handler.DataScopeHandler; +import org.springblade.core.datascope.handler.ScopeModelHandler; +import org.springblade.core.datascope.interceptor.DataScopeInterceptor; +import org.springblade.core.datascope.props.DataScopeProperties; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.jdbc.core.JdbcTemplate; + +/** + * 数据权限配置类 + * + * @author Chill + */ +@AutoConfiguration +@AllArgsConstructor +@EnableConfigurationProperties(DataScopeProperties.class) +public class DataScopeConfiguration { + + private final JdbcTemplate jdbcTemplate; + + @Bean + @ConditionalOnMissingBean(ScopeModelHandler.class) + public ScopeModelHandler scopeModelHandler() { + return new BladeScopeModelHandler(jdbcTemplate); + } + + @Bean + @ConditionalOnBean(ScopeModelHandler.class) + @ConditionalOnMissingBean(DataScopeHandler.class) + public DataScopeHandler dataScopeHandler(ScopeModelHandler scopeModelHandler) { + return new BladeDataScopeHandler(scopeModelHandler); + } + + @Bean + @ConditionalOnBean(DataScopeHandler.class) + @ConditionalOnMissingBean(DataScopeInterceptor.class) + public DataScopeInterceptor interceptor(DataScopeHandler dataScopeHandler, DataScopeProperties dataScopeProperties) { + return new DataScopeInterceptor(dataScopeHandler, dataScopeProperties); + } + +} diff --git a/blade-starter-datascope/src/main/java/org/springblade/core/datascope/constant/DataScopeConstant.java b/blade-starter-datascope/src/main/java/org/springblade/core/datascope/constant/DataScopeConstant.java new file mode 100644 index 0000000..866cadf --- /dev/null +++ b/blade-starter-datascope/src/main/java/org/springblade/core/datascope/constant/DataScopeConstant.java @@ -0,0 +1,74 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.datascope.constant; + +import org.springblade.core.tool.utils.StringUtil; + +/** + * 数据权限常量 + * + * @author Chill + */ +public interface DataScopeConstant { + + String DEFAULT_COLUMN = "create_dept"; + + /** + * 获取部门数据 + */ + String DATA_BY_DEPT = "select id from blade_dept where ancestors like concat(concat('%', ?),'%') and is_deleted = 0"; + + /** + * 根据resourceCode获取数据权限配置 + */ + String DATA_BY_CODE = "select resource_code, scope_column, scope_field, scope_type, scope_value from blade_scope_data where resource_code = ?"; + + /** + * 根据mapperId获取数据权限配置 + * + * @param size 数量 + * @return String + */ + static String dataByMapper(int size) { + return "select resource_code, scope_column, scope_field, scope_type, scope_value from blade_scope_data where scope_class = ? and id in (select scope_id from blade_role_scope where scope_category = 1 and role_id in (" + buildHolder(size) + "))"; + } + + /** + * 获取Sql占位符 + * + * @param size 数量 + * @return String + */ + static String buildHolder(int size) { + StringBuilder builder = StringUtil.builder(); + for (int i = 0; i < size; i++) { + builder.append("?,"); + } + return StringUtil.removeSuffix(builder.toString(), ","); + } + + +} diff --git a/blade-starter-datascope/src/main/java/org/springblade/core/datascope/enums/DataScopeEnum.java b/blade-starter-datascope/src/main/java/org/springblade/core/datascope/enums/DataScopeEnum.java new file mode 100644 index 0000000..b11e530 --- /dev/null +++ b/blade-starter-datascope/src/main/java/org/springblade/core/datascope/enums/DataScopeEnum.java @@ -0,0 +1,83 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.datascope.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; +import java.util.Optional; + +/** + * 数据权限类型 + * + * @author Chill + */ +@Getter +@AllArgsConstructor +public enum DataScopeEnum { + /** + * 全部数据 + */ + ALL(1, "全部"), + + /** + * 本人可见 + */ + OWN(2, "本人可见"), + + /** + * 所在机构可见 + */ + OWN_DEPT(3, "所在机构可见"), + + /** + * 所在机构及子级可见 + */ + OWN_DEPT_CHILD(4, "所在机构及子级可见"), + + /** + * 自定义 + */ + CUSTOM(5, "自定义"); + + /** + * 类型 + */ + private final int type; + /** + * 描述 + */ + private final String description; + + public static DataScopeEnum of(Integer dataScopeType) { + return Optional.ofNullable(dataScopeType) + .flatMap(type -> Arrays.stream(DataScopeEnum.values()) + .filter(scopeTypeEnum -> scopeTypeEnum.type == dataScopeType) + .findFirst()) + .orElse(null); + } +} diff --git a/blade-starter-datascope/src/main/java/org/springblade/core/datascope/exception/DataScopeException.java b/blade-starter-datascope/src/main/java/org/springblade/core/datascope/exception/DataScopeException.java new file mode 100644 index 0000000..88dcf7f --- /dev/null +++ b/blade-starter-datascope/src/main/java/org/springblade/core/datascope/exception/DataScopeException.java @@ -0,0 +1,45 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.datascope.exception; + +/** + * 数据权限异常 + * + * @author L.cm + */ +public class DataScopeException extends RuntimeException { + + public DataScopeException() { + } + + public DataScopeException(String message) { + super(message); + } + + public DataScopeException(Throwable cause) { + super(cause); + } +} diff --git a/blade-starter-datascope/src/main/java/org/springblade/core/datascope/handler/BladeDataScopeHandler.java b/blade-starter-datascope/src/main/java/org/springblade/core/datascope/handler/BladeDataScopeHandler.java new file mode 100644 index 0000000..7f6a0fd --- /dev/null +++ b/blade-starter-datascope/src/main/java/org/springblade/core/datascope/handler/BladeDataScopeHandler.java @@ -0,0 +1,93 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.datascope.handler; + +import lombok.RequiredArgsConstructor; +import org.springblade.core.datascope.enums.DataScopeEnum; +import org.springblade.core.datascope.model.DataScopeModel; +import org.springblade.core.secure.BladeUser; +import org.springblade.core.tool.constant.RoleConstant; +import org.springblade.core.tool.utils.BeanUtil; +import org.springblade.core.tool.utils.Func; +import org.springblade.core.tool.utils.PlaceholderUtil; +import org.springblade.core.tool.utils.StringUtil; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * 默认数据权限规则 + * + * @author Chill + */ +@RequiredArgsConstructor +public class BladeDataScopeHandler implements DataScopeHandler { + + private final ScopeModelHandler scopeModelHandler; + + @Override + public String sqlCondition(String mapperId, DataScopeModel dataScope, BladeUser bladeUser, String originalSql) { + + //数据权限资源编号 + String code = dataScope.getResourceCode(); + + //根据mapperId从数据库中获取对应模型 + DataScopeModel dataScopeDb = scopeModelHandler.getDataScopeByMapper(mapperId, bladeUser.getRoleId()); + + //mapperId配置未取到则从数据库中根据资源编号获取 + if (dataScopeDb == null && StringUtil.isNotBlank(code)) { + dataScopeDb = scopeModelHandler.getDataScopeByCode(code); + } + + //未从数据库找到对应配置则采用默认 + dataScope = (dataScopeDb != null) ? dataScopeDb : dataScope; + + //判断数据权限类型并组装对应Sql + Integer scopeRule = Objects.requireNonNull(dataScope).getScopeType(); + DataScopeEnum scopeTypeEnum = DataScopeEnum.of(scopeRule); + List ids = new ArrayList<>(); + String whereSql = "where scope.{} in ({})"; + if (DataScopeEnum.ALL == scopeTypeEnum || StringUtil.containsAny(bladeUser.getRoleName(), RoleConstant.ADMINISTRATOR)) { + return null; + } else if (DataScopeEnum.CUSTOM == scopeTypeEnum) { + whereSql = PlaceholderUtil.getDefaultResolver().resolveByMap(dataScope.getScopeValue(), BeanUtil.toMap(bladeUser)); + } else if (DataScopeEnum.OWN == scopeTypeEnum) { + ids.add(bladeUser.getUserId()); + } else if (DataScopeEnum.OWN_DEPT == scopeTypeEnum) { + ids.addAll(Func.toLongList(bladeUser.getDeptId())); + } else if (DataScopeEnum.OWN_DEPT_CHILD == scopeTypeEnum) { + List deptIds = Func.toLongList(bladeUser.getDeptId()); + ids.addAll(deptIds); + deptIds.forEach(deptId -> { + List deptIdList = scopeModelHandler.getDeptAncestors(deptId); + ids.addAll(deptIdList); + }); + } + return StringUtil.format("select {} from ({}) scope " + whereSql, Func.toStr(dataScope.getScopeField(), "*"), originalSql, dataScope.getScopeColumn(), StringUtil.join(ids)); + } + +} diff --git a/blade-starter-datascope/src/main/java/org/springblade/core/datascope/handler/BladeScopeModelHandler.java b/blade-starter-datascope/src/main/java/org/springblade/core/datascope/handler/BladeScopeModelHandler.java new file mode 100644 index 0000000..7add714 --- /dev/null +++ b/blade-starter-datascope/src/main/java/org/springblade/core/datascope/handler/BladeScopeModelHandler.java @@ -0,0 +1,127 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.datascope.handler; + +import lombok.RequiredArgsConstructor; +import org.springblade.core.cache.utils.CacheUtil; +import org.springblade.core.datascope.constant.DataScopeConstant; +import org.springblade.core.datascope.model.DataScopeModel; +import org.springblade.core.tool.utils.CollectionUtil; +import org.springblade.core.tool.utils.Func; +import org.springblade.core.tool.utils.StringPool; +import org.springblade.core.tool.utils.StringUtil; +import org.springframework.jdbc.core.BeanPropertyRowMapper; +import org.springframework.jdbc.core.JdbcTemplate; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.springblade.core.cache.constant.CacheConstant.SYS_CACHE; + +/** + * BladeScopeModelHandler + * + * @author Chill + */ +@RequiredArgsConstructor +public class BladeScopeModelHandler implements ScopeModelHandler { + + private static final String SCOPE_CACHE_CODE = "dataScope:code:"; + private static final String SCOPE_CACHE_CLASS = "dataScope:class:"; + private static final String DEPT_CACHE_ANCESTORS = "dept:ancestors:"; + private static final DataScopeModel SEARCHED_DATA_SCOPE_MODEL = new DataScopeModel(Boolean.TRUE); + + private final JdbcTemplate jdbcTemplate; + + /** + * 获取数据权限 + * + * @param mapperId 数据权限mapperId + * @param roleId 用户角色集合 + * @return DataScopeModel + */ + @Override + public DataScopeModel getDataScopeByMapper(String mapperId, String roleId) { + List args = new ArrayList<>(Collections.singletonList(mapperId)); + List roleIds = Func.toLongList(roleId); + args.addAll(roleIds); + // 增加searched字段防止未配置的参数重复读库导致缓存击穿 + // 后续若有新增配置则会清空缓存重新加载 + DataScopeModel dataScope = CacheUtil.get(SYS_CACHE, SCOPE_CACHE_CLASS, mapperId + StringPool.COLON + roleId, DataScopeModel.class, Boolean.FALSE); + if (dataScope == null || !dataScope.getSearched()) { + List list = jdbcTemplate.query(DataScopeConstant.dataByMapper(roleIds.size()), args.toArray(), new BeanPropertyRowMapper<>(DataScopeModel.class)); + if (CollectionUtil.isNotEmpty(list)) { + dataScope = list.iterator().next(); + dataScope.setSearched(Boolean.TRUE); + } else { + dataScope = SEARCHED_DATA_SCOPE_MODEL; + } + CacheUtil.put(SYS_CACHE, SCOPE_CACHE_CLASS, mapperId + StringPool.COLON + roleId, dataScope, Boolean.FALSE); + } + return StringUtil.isNotBlank(dataScope.getResourceCode()) ? dataScope : null; + } + + /** + * 获取数据权限 + * + * @param code 数据权限资源编号 + * @return DataScopeModel + */ + @Override + public DataScopeModel getDataScopeByCode(String code) { + DataScopeModel dataScope = CacheUtil.get(SYS_CACHE, SCOPE_CACHE_CODE, code, DataScopeModel.class, Boolean.FALSE); + // 增加searched字段防止未配置的参数重复读库导致缓存击穿 + // 后续若有新增配置则会清空缓存重新加载 + if (dataScope == null || !dataScope.getSearched()) { + List list = jdbcTemplate.query(DataScopeConstant.DATA_BY_CODE, new Object[]{code}, new BeanPropertyRowMapper<>(DataScopeModel.class)); + if (CollectionUtil.isNotEmpty(list)) { + dataScope = list.iterator().next(); + dataScope.setSearched(Boolean.TRUE); + } else { + dataScope = SEARCHED_DATA_SCOPE_MODEL; + } + CacheUtil.put(SYS_CACHE, SCOPE_CACHE_CODE, code, dataScope, Boolean.FALSE); + } + return StringUtil.isNotBlank(dataScope.getResourceCode()) ? dataScope : null; + } + + /** + * 获取部门子级 + * + * @param deptId 部门id + * @return deptIds + */ + @Override + public List getDeptAncestors(Long deptId) { + List ancestors = CacheUtil.get(SYS_CACHE, DEPT_CACHE_ANCESTORS, deptId, List.class); + if (CollectionUtil.isEmpty(ancestors)) { + ancestors = jdbcTemplate.queryForList(DataScopeConstant.DATA_BY_DEPT, new Object[]{deptId}, Long.class); + CacheUtil.put(SYS_CACHE, DEPT_CACHE_ANCESTORS, deptId, ancestors); + } + return ancestors; + } +} diff --git a/blade-starter-datascope/src/main/java/org/springblade/core/datascope/handler/DataScopeHandler.java b/blade-starter-datascope/src/main/java/org/springblade/core/datascope/handler/DataScopeHandler.java new file mode 100644 index 0000000..0cf804b --- /dev/null +++ b/blade-starter-datascope/src/main/java/org/springblade/core/datascope/handler/DataScopeHandler.java @@ -0,0 +1,49 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.datascope.handler; + +import org.springblade.core.datascope.model.DataScopeModel; +import org.springblade.core.secure.BladeUser; + +/** + * 数据权限规则 + * + * @author Chill + */ +public interface DataScopeHandler { + + /** + * 获取过滤sql + * + * @param mapperId 数据查询类 + * @param dataScope 数据权限类 + * @param bladeUser 当前用户信息 + * @param originalSql 原始Sql + * @return sql + */ + String sqlCondition(String mapperId, DataScopeModel dataScope, BladeUser bladeUser, String originalSql); + +} diff --git a/blade-starter-datascope/src/main/java/org/springblade/core/datascope/handler/ScopeModelHandler.java b/blade-starter-datascope/src/main/java/org/springblade/core/datascope/handler/ScopeModelHandler.java new file mode 100644 index 0000000..cbcb31b --- /dev/null +++ b/blade-starter-datascope/src/main/java/org/springblade/core/datascope/handler/ScopeModelHandler.java @@ -0,0 +1,64 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.datascope.handler; + +import org.springblade.core.datascope.model.DataScopeModel; + +import java.util.List; + +/** + * 获取数据权限模型统一接口 + * + * @author Chill + */ +public interface ScopeModelHandler { + + /** + * 获取数据权限 + * + * @param mapperId 数据权限mapperId + * @param roleId 用户角色集合 + * @return DataScopeModel + */ + DataScopeModel getDataScopeByMapper(String mapperId, String roleId); + + /** + * 获取数据权限 + * + * @param code 数据权限资源编号 + * @return DataScopeModel + */ + DataScopeModel getDataScopeByCode(String code); + + /** + * 获取部门子级 + * + * @param deptId 部门id + * @return deptIds + */ + List getDeptAncestors(Long deptId); + +} diff --git a/blade-starter-datascope/src/main/java/org/springblade/core/datascope/interceptor/DataScopeInterceptor.java b/blade-starter-datascope/src/main/java/org/springblade/core/datascope/interceptor/DataScopeInterceptor.java new file mode 100644 index 0000000..1d5236c --- /dev/null +++ b/blade-starter-datascope/src/main/java/org/springblade/core/datascope/interceptor/DataScopeInterceptor.java @@ -0,0 +1,148 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.datascope.interceptor; + +import com.baomidou.mybatisplus.core.toolkit.PluginUtils; +import com.baomidou.mybatisplus.core.toolkit.StringPool; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.ibatis.executor.Executor; +import org.apache.ibatis.mapping.BoundSql; +import org.apache.ibatis.mapping.MappedStatement; +import org.apache.ibatis.mapping.SqlCommandType; +import org.apache.ibatis.mapping.StatementType; +import org.apache.ibatis.session.ResultHandler; +import org.apache.ibatis.session.RowBounds; +import org.springblade.core.datascope.annotation.DataAuth; +import org.springblade.core.datascope.handler.DataScopeHandler; +import org.springblade.core.datascope.model.DataScopeModel; +import org.springblade.core.datascope.props.DataScopeProperties; +import org.springblade.core.mp.intercept.QueryInterceptor; +import org.springblade.core.secure.BladeUser; +import org.springblade.core.secure.utils.AuthUtil; +import org.springblade.core.tool.utils.ClassUtil; +import org.springblade.core.tool.utils.SpringUtil; +import org.springblade.core.tool.utils.StringUtil; + +import java.lang.reflect.Method; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + + +/** + * mybatis 数据权限拦截器 + * + * @author L.cm, Chill + */ +@Slf4j +@RequiredArgsConstructor +@SuppressWarnings({"rawtypes"}) +public class DataScopeInterceptor implements QueryInterceptor { + + private final ConcurrentMap dataAuthMap = new ConcurrentHashMap<>(8); + + private final DataScopeHandler dataScopeHandler; + private final DataScopeProperties dataScopeProperties; + + @Override + public void intercept(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { + //未启用则放行 + if (!dataScopeProperties.getEnabled()) { + return; + } + + //未取到用户则放行 + BladeUser bladeUser = AuthUtil.getUser(); + if (bladeUser == null) { + return; + } + + if (SqlCommandType.SELECT != ms.getSqlCommandType() || StatementType.CALLABLE == ms.getStatementType()) { + return; + } + + String originalSql = boundSql.getSql(); + + //查找注解中包含DataAuth类型的参数 + DataAuth dataAuth = findDataAuthAnnotation(ms); + + //注解为空并且数据权限方法名未匹配到,则放行 + String mapperId = ms.getId(); + String className = mapperId.substring(0, mapperId.lastIndexOf(StringPool.DOT)); + String mapperName = ClassUtil.getShortName(className); + String methodName = mapperId.substring(mapperId.lastIndexOf(StringPool.DOT) + 1); + boolean mapperSkip = dataScopeProperties.getMapperKey().stream().noneMatch(methodName::contains) + || dataScopeProperties.getMapperExclude().stream().anyMatch(mapperName::contains); + if (dataAuth == null && mapperSkip) { + return; + } + + //创建数据权限模型 + DataScopeModel dataScope = new DataScopeModel(); + + //若注解不为空,则配置注解项 + if (dataAuth != null) { + dataScope.setResourceCode(dataAuth.code()); + dataScope.setScopeColumn(dataAuth.column()); + dataScope.setScopeType(dataAuth.type().getType()); + dataScope.setScopeField(dataAuth.field()); + dataScope.setScopeValue(dataAuth.value()); + } + + //获取数据权限规则对应的筛选Sql + String sqlCondition = dataScopeHandler.sqlCondition(mapperId, dataScope, bladeUser, originalSql); + if (!StringUtil.isBlank(sqlCondition)) { + PluginUtils.MPBoundSql mpBoundSql = PluginUtils.mpBoundSql(boundSql); + mpBoundSql.sql(sqlCondition); + } + } + + /** + * 获取数据权限注解信息 + * + * @param mappedStatement mappedStatement + * @return DataAuth + */ + private DataAuth findDataAuthAnnotation(MappedStatement mappedStatement) { + String id = mappedStatement.getId(); + return dataAuthMap.computeIfAbsent(id, (key) -> { + String className = key.substring(0, key.lastIndexOf(StringPool.DOT)); + String mapperBean = StringUtil.firstCharToLower(ClassUtil.getShortName(className)); + Object mapper = SpringUtil.getBean(mapperBean); + String methodName = key.substring(key.lastIndexOf(StringPool.DOT) + 1); + Class[] interfaces = ClassUtil.getAllInterfaces(mapper); + for (Class mapperInterface : interfaces) { + for (Method method : mapperInterface.getDeclaredMethods()) { + if (methodName.equals(method.getName()) && method.isAnnotationPresent(DataAuth.class)) { + return method.getAnnotation(DataAuth.class); + } + } + } + return null; + }); + } + +} diff --git a/blade-starter-datascope/src/main/java/org/springblade/core/datascope/model/DataScopeModel.java b/blade-starter-datascope/src/main/java/org/springblade/core/datascope/model/DataScopeModel.java new file mode 100644 index 0000000..f23217a --- /dev/null +++ b/blade-starter-datascope/src/main/java/org/springblade/core/datascope/model/DataScopeModel.java @@ -0,0 +1,79 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.datascope.model; + +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springblade.core.datascope.constant.DataScopeConstant; +import org.springblade.core.datascope.enums.DataScopeEnum; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 数据权限实体类 + * + * @author Chill + */ +@Data +@NoArgsConstructor +public class DataScopeModel implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 构造器创建 + */ + public DataScopeModel(Boolean searched) { + this.searched = searched; + } + + /** + * 是否已查询 + */ + private Boolean searched = Boolean.FALSE; + /** + * 资源编号 + */ + private String resourceCode; + /** + * 数据权限字段 + */ + private String scopeColumn = DataScopeConstant.DEFAULT_COLUMN; + /** + * 数据权限规则 + */ + private Integer scopeType = DataScopeEnum.ALL.getType(); + /** + * 可见字段 + */ + private String scopeField; + /** + * 数据权限规则值 + */ + private String scopeValue; +} diff --git a/blade-starter-datascope/src/main/java/org/springblade/core/datascope/props/DataScopeProperties.java b/blade-starter-datascope/src/main/java/org/springblade/core/datascope/props/DataScopeProperties.java new file mode 100644 index 0000000..e633128 --- /dev/null +++ b/blade-starter-datascope/src/main/java/org/springblade/core/datascope/props/DataScopeProperties.java @@ -0,0 +1,58 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.datascope.props; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * 数据权限参数配置类 + * + * @author Chill + */ +@Data +@ConfigurationProperties(prefix = "blade.data-scope") +public class DataScopeProperties { + + /** + * 开启数据权限 + */ + private Boolean enabled = true; + /** + * mapper方法匹配关键字 + */ + private List mapperKey = Arrays.asList("page", "Page", "list", "List"); + + /** + * mapper过滤 + */ + private List mapperExclude = Collections.singletonList("FlowMapper"); + +} diff --git a/blade-starter-develop/pom.xml b/blade-starter-develop/pom.xml new file mode 100644 index 0000000..b34e934 --- /dev/null +++ b/blade-starter-develop/pom.xml @@ -0,0 +1,40 @@ + + + 4.0.0 + + BladeX-Tool + org.springblade + ${revision} + + + blade-starter-develop + ${project.artifactId} + ${project.parent.version} + jar + + + + + org.springblade + blade-core-tool + + + + com.baomidou + mybatis-plus-generator + + + com.baomidou + mybatis-plus-extension + + + + com.ibeetl + beetl + 3.10.0.Antlr4.5-RELEASE + + + + diff --git a/blade-starter-develop/src/main/java/org/springblade/develop/CodeGenerator.java b/blade-starter-develop/src/main/java/org/springblade/develop/CodeGenerator.java new file mode 100644 index 0000000..ce90bd5 --- /dev/null +++ b/blade-starter-develop/src/main/java/org/springblade/develop/CodeGenerator.java @@ -0,0 +1,109 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.develop; + + +import org.springblade.develop.constant.DevelopConstant; +import org.springblade.develop.support.BladeCodeGenerator; + +/** + * 代码生成器 + * + * @author Chill + */ +public class CodeGenerator { + + /** + * 代码生成的模块名 + */ + public static String CODE_NAME = "应用管理"; + /** + * 代码所在服务名 + */ + public static String SERVICE_NAME = "blade-system"; + /** + * 代码生成的包名 + */ + public static String PACKAGE_NAME = "org.springblade.system"; + /** + * 前端代码生成风格 + */ + public static String CODE_STYLE = DevelopConstant.SABER_NAME; + /** + * 前端代码生成地址 + */ + public static String PACKAGE_WEB_DIR = "/Users/chill/Workspaces/product/Saber"; + /** + * 需要去掉的表前缀 + */ + public static String[] TABLE_PREFIX = {"blade_"}; + /** + * 需要生成的表名(两者只能取其一) + */ + public static String[] INCLUDE_TABLES = {"blade_client"}; + /** + * 需要排除的表名(两者只能取其一) + */ + public static String[] EXCLUDE_TABLES = {}; + /** + * 是否包含基础业务字段 + */ + public static Boolean HAS_SUPER_ENTITY = Boolean.TRUE; + /** + * 基础业务字段 + */ + public static String[] SUPER_ENTITY_COLUMNS = {"id", "create_time", "create_user", "create_dept", "update_time", "update_user", "status", "is_deleted"}; + /** + * 是否包含包装器 + */ + public static Boolean HAS_WRAPPER = Boolean.TRUE; + /** + * 是否包含远程调用 + */ + public static Boolean HAS_FEIGN = Boolean.FALSE; + + + /** + * RUN THIS + */ + public static void run() { + BladeCodeGenerator generator = new BladeCodeGenerator(); + generator.setCodeName(CODE_NAME); + generator.setServiceName(SERVICE_NAME); + generator.setCodeStyle(CODE_STYLE); + generator.setPackageName(PACKAGE_NAME); + generator.setPackageWebDir(PACKAGE_WEB_DIR); + generator.setTablePrefix(TABLE_PREFIX); + generator.setIncludeTables(INCLUDE_TABLES); + generator.setExcludeTables(EXCLUDE_TABLES); + generator.setHasSuperEntity(HAS_SUPER_ENTITY); + generator.setSuperEntityColumns(SUPER_ENTITY_COLUMNS); + generator.setHasWrapper(HAS_WRAPPER); + generator.setHasFeign(HAS_FEIGN); + generator.run(); + } + +} diff --git a/blade-starter-develop/src/main/java/org/springblade/develop/constant/DevelopConstant.java b/blade-starter-develop/src/main/java/org/springblade/develop/constant/DevelopConstant.java new file mode 100644 index 0000000..2b93e14 --- /dev/null +++ b/blade-starter-develop/src/main/java/org/springblade/develop/constant/DevelopConstant.java @@ -0,0 +1,83 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.develop.constant; + +/** + * 代码生成系统常量. + * + * @author Chill + */ +public interface DevelopConstant { + /** + * sword 系统名 + */ + String SWORD_NAME = "sword"; + + /** + * saber 系统名 + */ + String SABER_NAME = "saber"; + + /** + * saber3 系统名 + */ + String SABER3_NAME = "saber3"; + + /** + * lemon 系统名 + */ + String LEMON_NAME = "lemon"; + + /** + * element 系统名 + */ + String ELEMENT_NAME = "element"; + + /** + * element-plus 系统名 + */ + String ELEMENT_PLUS_NAME = "element-plus"; + + /** + * 单表模式 + */ + String TEMPLATE_CRUD = "crud"; + + /** + * 树表模式 + */ + String TEMPLATE_TREE = "tree"; + + /** + * 主子表模式 + */ + String TEMPLATE_SUB = "sub"; + + /** + * 主模块 + */ + String TEMPLATE_MAIN = "main"; +} diff --git a/blade-starter-develop/src/main/java/org/springblade/develop/support/BladeCodeGenerator.java b/blade-starter-develop/src/main/java/org/springblade/develop/support/BladeCodeGenerator.java new file mode 100644 index 0000000..d90f1ef --- /dev/null +++ b/blade-starter-develop/src/main/java/org/springblade/develop/support/BladeCodeGenerator.java @@ -0,0 +1,381 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.develop.support; + +import com.baomidou.mybatisplus.generator.FastAutoGenerator; +import com.baomidou.mybatisplus.generator.config.TemplateType; +import com.baomidou.mybatisplus.generator.config.rules.DateType; +import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.apache.ibatis.annotations.Mapper; +import org.springblade.core.tool.utils.Func; +import org.springblade.core.tool.utils.StringUtil; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PropertiesLoaderUtils; + +import java.io.IOException; +import java.util.*; +import java.util.stream.Collectors; + +import static org.springblade.develop.constant.DevelopConstant.*; + +/** + * 代码生成器配置类 + * + * @author Chill + */ +@Data +@Slf4j +public class BladeCodeGenerator { + /** + * 代码风格 + */ + private String codeStyle = SABER_NAME; + /** + * 代码模块名称 + */ + private String codeName; + /** + * 模型编号 + */ + private String modelCode; + /** + * 模型实体类 + */ + private String modelClass; + /** + * 代码所在服务名 + */ + private String serviceName = "blade-service"; + /** + * 代码生成的包名 + */ + private String packageName = "org.springblade.test"; + /** + * 模版类型 + */ + private String templateType; + /** + * 作者信息 + */ + private String author; + /** + * 子表模型主键 + */ + private String subModelId; + /** + * 子表绑定外键 + */ + private String subFkId; + /** + * 树主键字段 + */ + private String treeId; + /** + * 树父主键字段 + */ + private String treePid; + /** + * 树名称字段 + */ + private String treeName; + /** + * 代码后端生成的地址 + */ + private String packageDir; + /** + * 代码前端生成的地址 + */ + private String packageWebDir; + /** + * 需要去掉的表前缀 + */ + private String[] tablePrefix = {"blade_"}; + /** + * 需要生成的表名(两者只能取其一) + */ + private String[] includeTables = {"blade_test"}; + /** + * 需要排除的表名(两者只能取其一) + */ + private String[] excludeTables = {}; + /** + * 是否包含基础业务字段 + */ + private Boolean hasSuperEntity = Boolean.TRUE; + /** + * 是否包含包装器 + */ + private Boolean hasWrapper = Boolean.TRUE; + /** + * 是否包含远程调用 + */ + private Boolean hasFeign = Boolean.FALSE; + /** + * 是否包含服务名 + */ + private Boolean hasServiceName = Boolean.FALSE; + /** + * 基础业务字段 + */ + private String[] superEntityColumns = {"create_time", "create_user", "create_dept", "update_time", "update_user", "status", "is_deleted"}; + /** + * 租户字段 + */ + private String tenantColumn = "tenant_id"; + /** + * 数据库驱动名 + */ + private String driverName; + /** + * 数据库链接地址 + */ + private String url; + /** + * 数据库用户名 + */ + private String username; + /** + * 数据库密码 + */ + private String password; + /** + * 数据模型 + */ + private Map model; + /** + * 数据原型 + */ + private List> prototypes; + /** + * 子数据模型 + */ + private Map subModel; + /** + * 子数据原型 + */ + private List> subPrototypes; + + /** + * 代码生成执行 + */ + public void run() { + // 主模块代码生成 + getAutoGenerator(getCustomMap(TEMPLATE_MAIN), getCustomFile(TEMPLATE_MAIN)).templateEngine(new BladeTemplateEngine(getOutputDir(), getOutputWebDir())).execute(); + // 子模块代码生成 + if (Func.equals(templateType, TEMPLATE_SUB) && StringUtil.isNotBlank(subModelId)) { + getAutoGenerator(getCustomMap(TEMPLATE_SUB), getCustomFile(TEMPLATE_SUB)).templateEngine(new BladeTemplateEngine(getOutputDir(), getOutputWebDir())).execute(); + } + } + + /** + * 设置 customMap + */ + private Map getCustomMap(String generateType) { + List> prototypeList; + String[] split = packageName.split("\\."); + String serviceCode = split[split.length - 1]; + Map customMap = new HashMap<>(11); + customMap.put("generateType", generateType); + customMap.put("codeName", codeName); + customMap.put("serviceName", serviceName); + customMap.put("serviceCode", serviceCode); + customMap.put("packageName", packageName); + customMap.put("tenantColumn", tenantColumn); + customMap.put("hasWrapper", hasWrapper); + customMap.put("hasServiceName", hasServiceName); + customMap.put("hasSuperEntity", hasSuperEntity); + customMap.put("templateType", templateType); + customMap.put("author", author); + customMap.put("subModelId", subModelId); + customMap.put("subFkId", subFkId); + customMap.put("treeId", treeId); + customMap.put("treePid", treePid); + customMap.put("treeName", treeName); + customMap.put("subFkIdHump", StringUtil.underlineToHump(subFkId)); + customMap.put("treeIdHump", StringUtil.underlineToHump(treeId)); + customMap.put("treePidHump", StringUtil.underlineToHump(treePid)); + if (Func.equals(generateType, TEMPLATE_SUB)) { + prototypeList = subPrototypes; + customMap.put("model", subModel); + customMap.put("prototypes", subPrototypes); + customMap.put("modelCode", subModel.get("modelCode")); + customMap.put("modelClass", subModel.get("modelClass")); + customMap.put("modelTable", subModel.get("modelTable")); + } else { + prototypeList = prototypes; + customMap.put("model", model); + customMap.put("prototypes", prototypes); + customMap.put("subModel", subModel); + customMap.put("subPrototypes", subPrototypes); + customMap.put("modelCode", model.get("modelCode")); + customMap.put("modelClass", model.get("modelClass")); + customMap.put("modelTable", model.get("modelTable")); + } + List propertyImport = prototypeList.stream().filter(prototype -> { + String propertyType = String.valueOf(prototype.get("propertyType")); + return !"String".equals(propertyType) && !"Integer".equals(propertyType) && !"Long".equals(propertyType); + }).map(prototype -> String.valueOf(prototype.get("propertyEntity"))).distinct().collect(Collectors.toList()); + customMap.put("propertyImport", propertyImport); + return customMap; + } + + /** + * 设置 customFile + */ + private Map getCustomFile(String type) { + Map customFile = new HashMap<>(15); + if (!Func.equals(type, TEMPLATE_SUB)) { + customFile.put("menu.sql", "/templates/sql/menu.sql.btl"); + } + customFile.put("entityVO.java", "/templates/api/entityVO.java.btl"); + customFile.put("entityDTO.java", "/templates/api/entityDTO.java.btl"); + customFile.put("entityExcel.java", "/templates/api/entityExcel.java.btl"); + if (hasWrapper) { + customFile.put("wrapper.java", "/templates/api/wrapper.java.btl"); + } + if (hasFeign) { + customFile.put("feign.java", "/templates/api/feign.java.btl"); + customFile.put("feignclient.java", "/templates/api/feignclient.java.btl"); + } + if (Func.isNotBlank(packageWebDir)) { + if (Func.equals(codeStyle, SWORD_NAME)) { + customFile.put("action.js", "/templates/sword/action.js.btl"); + customFile.put("model.js", "/templates/sword/model.js.btl"); + customFile.put("service.js", "/templates/sword/service.js.btl"); + customFile.put("list.js", "/templates/sword/list.js.btl"); + customFile.put("add.js", "/templates/sword/add.js.btl"); + customFile.put("edit.js", "/templates/sword/edit.js.btl"); + customFile.put("view.js", "/templates/sword/view.js.btl"); + } else if (Func.equals(codeStyle, SABER_NAME)) { + customFile.put("api.js", "/templates/saber/" + templateType + "/api.js.btl"); + customFile.put("option.js", "/templates/saber/" + templateType + "/option.js.btl"); + if (!Func.equals(type, TEMPLATE_SUB)) { + customFile.put("crud.vue", "/templates/saber/" + templateType + "/crud.vue.btl"); + } + } else if (Func.equals(codeStyle, SABER3_NAME)) { + customFile.put("api.js", "/templates/saber3/" + templateType + "/api.js.btl"); + customFile.put("option.js", "/templates/saber3/" + templateType + "/option.js.btl"); + if (!Func.equals(type, TEMPLATE_SUB)) { + customFile.put("crud.vue", "/templates/saber3/" + templateType + "/crud.vue.btl"); + } + } else if (Func.equals(codeStyle, ELEMENT_NAME)) { + customFile.put("api.js", "/templates/element/" + templateType + "/api.js.btl"); + customFile.put("option.js", "/templates/element/" + templateType + "/option.js.btl"); + if (!Func.equals(type, TEMPLATE_SUB)) { + customFile.put("crud.vue", "/templates/element/" + templateType + "/crud.vue.btl"); + } else { + customFile.put("sub.vue", "/templates/element/" + templateType + "/sub.vue.btl"); + } + } else if (Func.equals(codeStyle, ELEMENT_PLUS_NAME)) { + customFile.put("api.js", "/templates/element-plus/" + templateType + "/api.js.btl"); + customFile.put("option.js", "/templates/element-plus/" + templateType + "/option.js.btl"); + if (!Func.equals(type, TEMPLATE_SUB)) { + customFile.put("crud.vue", "/templates/element-plus/" + templateType + "/crud.vue.btl"); + } else { + customFile.put("sub.vue", "/templates/element-plus/" + templateType + "/sub.vue.btl"); + } + } else if (Func.equals(codeStyle, LEMON_NAME)) { + customFile.put("data.ts", "/templates/lemon/" + templateType + "/data.ts.btl"); + customFile.put("Modal.vue", "/templates/lemon/" + templateType + "/Modal.vue.btl"); + customFile.put("data.data.ts", "/templates/lemon/" + templateType + "/data.data.ts.btl"); + if (!Func.equals(type, TEMPLATE_SUB)) { + customFile.put("index.vue", "/templates/lemon/" + templateType + "/index.vue.btl"); + } else { + customFile.put("lemonSub.vue", "/templates/lemon/" + templateType + "/sub.vue.btl"); + } + + } + } + return customFile; + } + + private FastAutoGenerator getAutoGenerator(Map customMap, Map customFile) { + Properties props = getProperties(); + String url = Func.toStr(this.url, props.getProperty("spring.datasource.url")); + String username = Func.toStr(this.username, props.getProperty("spring.datasource.username")); + String password = Func.toStr(this.password, props.getProperty("spring.datasource.password")); + return FastAutoGenerator.create(url, username, password) + .globalConfig(builder -> builder.author(StringUtil.isBlank(author) ? props.getProperty("author") : author).dateType(DateType.TIME_PACK).enableSwagger().outputDir(getOutputDir()).disableOpenDir()) + .packageConfig(builder -> builder.parent(packageName).controller("controller").entity("pojo.entity").service("service").serviceImpl("service.impl").mapper("mapper").xml("mapper")) + .strategyConfig(builder -> builder.addTablePrefix(tablePrefix).addInclude(Func.toStrArray(String.valueOf(customMap.get("modelTable")))).addExclude(excludeTables) + .entityBuilder().naming(NamingStrategy.underline_to_camel).columnNaming(NamingStrategy.underline_to_camel).enableLombok().superClass("org.springblade.core.mp.base.BaseEntity").formatFileName("%sEntity").addSuperEntityColumns(superEntityColumns).enableFileOverride() + .serviceBuilder().superServiceClass("org.springblade.core.mp.base.BaseService").superServiceImplClass("org.springblade.core.mp.base.BaseServiceImpl").formatServiceFileName("I%sService").formatServiceImplFileName("%sServiceImpl").enableFileOverride() + .mapperBuilder().mapperAnnotation(Mapper.class).enableBaseResultMap().enableBaseColumnList().formatMapperFileName("%sMapper").formatXmlFileName("%sMapper").enableFileOverride() + .controllerBuilder().superClass("org.springblade.core.boot.ctrl.BladeController").formatFileName("%sController").enableRestStyle().enableHyphenStyle().enableFileOverride() + ) + .templateConfig(builder -> builder.disable(TemplateType.ENTITY) + .entity("/templates/api/entity.java") + .service("/templates/api/service.java") + .serviceImpl("/templates/api/serviceImpl.java") + .mapper("/templates/api/mapper.java") + .xml("/templates/api/mapper.xml") + .controller("/templates/api/controller.java")) + .injectionConfig(builder -> builder.beforeOutputFile( + (tableInfo, objectMap) -> System.out.println("tableInfo: " + tableInfo.getEntityName() + " objectMap: " + objectMap.size()) + ).customMap(customMap).customFile(customFile) + ); + } + + /** + * 获取配置文件 + * + * @return 配置Props + */ + private Properties getProperties() { + // 读取配置文件 + Resource resource = new ClassPathResource("/templates/code.properties"); + Properties props = new Properties(); + try { + props = PropertiesLoaderUtils.loadProperties(resource); + } catch (IOException e) { + e.printStackTrace(); + } + return props; + } + + /** + * 生成到项目中 + * + * @return outputDir + */ + public String getOutputDir() { + return (Func.isBlank(packageDir) ? System.getProperty("user.dir") + "/blade-ops/blade-develop" : packageDir) + "/src/main/java"; + } + + + /** + * 生成到Web项目中 + * + * @return outputDir + */ + public String getOutputWebDir() { + return (Func.isBlank(packageWebDir) ? System.getProperty("user.dir") : packageWebDir) + "/src"; + } + +} diff --git a/blade-starter-develop/src/main/java/org/springblade/develop/support/BladeTemplateEngine.java b/blade-starter-develop/src/main/java/org/springblade/develop/support/BladeTemplateEngine.java new file mode 100644 index 0000000..a3b256f --- /dev/null +++ b/blade-starter-develop/src/main/java/org/springblade/develop/support/BladeTemplateEngine.java @@ -0,0 +1,166 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.develop.support; + +import com.baomidou.mybatisplus.core.toolkit.IdWorker; +import com.baomidou.mybatisplus.core.toolkit.StringPool; +import com.baomidou.mybatisplus.generator.config.OutputFile; +import com.baomidou.mybatisplus.generator.config.builder.CustomFile; +import com.baomidou.mybatisplus.generator.config.po.TableInfo; +import com.baomidou.mybatisplus.generator.engine.BeetlTemplateEngine; +import lombok.AllArgsConstructor; +import org.springblade.core.tool.utils.StringUtil; + +import java.io.File; +import java.util.List; +import java.util.Map; + +/** + * 代码模版生成实现类 + * + * @author Chill + */ +@AllArgsConstructor +public class BladeTemplateEngine extends BeetlTemplateEngine { + + private String outputDir; + private String outputWebDir; + + @Override + protected void outputCustomFile(List customFiles, TableInfo tableInfo, Map objectMap) { + String packageName = String.valueOf(objectMap.get("packageName")); + String serviceCode = String.valueOf(objectMap.get("serviceCode")); + String modelCode = String.valueOf(objectMap.get("modelCode")); + String entityName = String.valueOf(objectMap.get("modelClass")); + String entityNameLower = entityName.toLowerCase(); + + customFiles.forEach(customFile -> { + String key = customFile.getFileName(); + String value = customFile.getTemplatePath(); + String outputPath = getPathInfo(OutputFile.parent); + objectMap.put("entityKey", entityNameLower); + if (StringUtil.equals(key, "menu.sql")) { + objectMap.put("menuId", IdWorker.getId()); + objectMap.put("addMenuId", IdWorker.getId()); + objectMap.put("editMenuId", IdWorker.getId()); + objectMap.put("removeMenuId", IdWorker.getId()); + objectMap.put("viewMenuId", IdWorker.getId()); + outputPath = outputDir + StringPool.SLASH + "sql" + StringPool.SLASH + entityNameLower + ".menu.sql"; + } + if (StringUtil.equals(key, "entityVO.java")) { + outputPath = outputDir + StringPool.SLASH + packageName.replace(StringPool.DOT, StringPool.SLASH) + StringPool.SLASH + "/pojo/vo" + StringPool.SLASH + entityName + "VO" + StringPool.DOT_JAVA; + } + + if (StringUtil.equals(key, "entityDTO.java")) { + outputPath = outputDir + StringPool.SLASH + packageName.replace(StringPool.DOT, StringPool.SLASH) + StringPool.SLASH + "/pojo/dto" + StringPool.SLASH + entityName + "DTO" + StringPool.DOT_JAVA; + } + + if (StringUtil.equals(key, "entityExcel.java")) { + outputPath = outputDir + StringPool.SLASH + packageName.replace(StringPool.DOT, StringPool.SLASH) + StringPool.SLASH + "/excel" + StringPool.SLASH + entityName + "Excel" + StringPool.DOT_JAVA; + } + + if (StringUtil.equals(key, "wrapper.java")) { + outputPath = outputDir + StringPool.SLASH + packageName.replace(StringPool.DOT, StringPool.SLASH) + StringPool.SLASH + "wrapper" + StringPool.SLASH + entityName + "Wrapper" + StringPool.DOT_JAVA; + } + + if (StringUtil.equals(key, "feign.java")) { + outputPath = outputDir + StringPool.SLASH + packageName.replace(StringPool.DOT, StringPool.SLASH) + StringPool.SLASH + "feign" + StringPool.SLASH + "I" + entityName + "Client" + StringPool.DOT_JAVA; + } + + if (StringUtil.equals(key, "feignclient.java")) { + outputPath = outputDir + StringPool.SLASH + packageName.replace(StringPool.DOT, StringPool.SLASH) + StringPool.SLASH + "feign" + StringPool.SLASH + entityName + "Client" + StringPool.DOT_JAVA; + } + + if (StringUtil.equals(key, "action.js")) { + outputPath = outputWebDir + StringPool.SLASH + "actions" + StringPool.SLASH + entityNameLower + ".js"; + } + + if (StringUtil.equals(key, "model.js")) { + outputPath = outputWebDir + StringPool.SLASH + "models" + StringPool.SLASH + entityNameLower + ".js"; + } + + if (StringUtil.equals(key, "service.js")) { + outputPath = outputWebDir + StringPool.SLASH + "services" + StringPool.SLASH + entityNameLower + ".js"; + } + + if (StringUtil.equals(key, "list.js")) { + outputPath = outputWebDir + StringPool.SLASH + "pages" + StringPool.SLASH + StringUtil.firstCharToUpper(modelCode) + StringPool.SLASH + entityName + StringPool.SLASH + entityName + ".js"; + } + + if (StringUtil.equals(key, "add.js")) { + outputPath = outputWebDir + StringPool.SLASH + "pages" + StringPool.SLASH + StringUtil.firstCharToUpper(modelCode) + StringPool.SLASH + entityName + StringPool.SLASH + entityName + "Add.js"; + } + + if (StringUtil.equals(key, "edit.js")) { + outputPath = outputWebDir + StringPool.SLASH + "pages" + StringPool.SLASH + StringUtil.firstCharToUpper(modelCode) + StringPool.SLASH + entityName + StringPool.SLASH + entityName + "Edit.js"; + } + + if (StringUtil.equals(key, "view.js")) { + outputPath = outputWebDir + StringPool.SLASH + "pages" + StringPool.SLASH + StringUtil.firstCharToUpper(modelCode) + StringPool.SLASH + entityName + StringPool.SLASH + entityName + "View.js"; + } + + if (StringUtil.equals(key, "api.js")) { + outputPath = outputWebDir + StringPool.SLASH + "api" + StringPool.SLASH + serviceCode + StringPool.SLASH + modelCode + ".js"; + } + + if (StringUtil.equals(key, "option.js")) { + outputPath = outputWebDir + StringPool.SLASH + "option" + StringPool.SLASH + serviceCode + StringPool.SLASH + modelCode + ".js"; + } + + if (StringUtil.equals(key, "crud.vue")) { + outputPath = outputWebDir + StringPool.SLASH + "views" + StringPool.SLASH + serviceCode + StringPool.SLASH + modelCode + ".vue"; + } + + if (StringUtil.equals(key, "sub.vue")) { + outputPath = outputWebDir + StringPool.SLASH + "views" + StringPool.SLASH + serviceCode + StringPool.SLASH + modelCode + "Sub.vue"; + } + + if (StringUtil.equals(key, "data.ts")) { + outputPath = outputWebDir + StringPool.SLASH + "api" + StringPool.SLASH + serviceCode + StringPool.SLASH + modelCode + ".ts"; + } + + if (StringUtil.equals(key, "data.data.ts")) { + + outputPath = outputWebDir + StringPool.SLASH + "views" + StringPool.SLASH + serviceCode + StringPool.SLASH + modelCode + StringPool.SLASH + modelCode + ".data.ts"; + } + + if (StringUtil.equals(key, "index.vue")) { + outputPath = outputWebDir + StringPool.SLASH + "views" + StringPool.SLASH + serviceCode + StringPool.SLASH + modelCode + StringPool.SLASH + "index.vue"; + } + + if (StringUtil.equals(key, "Modal.vue")) { + outputPath = outputWebDir + StringPool.SLASH + "views" + StringPool.SLASH + serviceCode + StringPool.SLASH + modelCode + StringPool.SLASH + entityName + "Modal.vue"; + } + + if (StringUtil.equals(key, "lemonSub.vue")) { + outputPath = outputWebDir + StringPool.SLASH + "views" + StringPool.SLASH + serviceCode + StringPool.SLASH + modelCode + StringPool.SLASH + entityName + "Sub.vue"; + } + outputFile(new File(String.valueOf(outputPath)), objectMap, value, Boolean.TRUE); + }); + } + + +} diff --git a/blade-starter-develop/src/main/resources/beetl.properties b/blade-starter-develop/src/main/resources/beetl.properties new file mode 100644 index 0000000..0d2e6d1 --- /dev/null +++ b/blade-starter-develop/src/main/resources/beetl.properties @@ -0,0 +1,10 @@ +#默认配置 +ENGINE = org.beetl.core.engine.FastRuntimeEngine + +#开始结束占位符 +DELIMITER_PLACEHOLDER_START = ${ +DELIMITER_PLACEHOLDER_END = } + +#开始结束标签 +DELIMITER_STATEMENT_START = # +DELIMITER_STATEMENT_END = null diff --git a/blade-starter-develop/src/main/resources/templates/api/controller.java.btl b/blade-starter-develop/src/main/resources/templates/api/controller.java.btl new file mode 100644 index 0000000..1b63706 --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/api/controller.java.btl @@ -0,0 +1,253 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package ${package.Controller}; + +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; +import lombok.AllArgsConstructor; +import jakarta.validation.Valid; + +import org.springblade.core.secure.BladeUser; +import org.springblade.core.secure.annotation.PreAuth; +import org.springblade.core.mp.support.Condition; +import org.springblade.core.mp.support.Query; +import org.springblade.core.tool.api.R; +import org.springblade.core.tool.utils.Func; +import org.springframework.web.bind.annotation.*; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import ${packageName!}.pojo.entity.${modelClass!}Entity; +import ${packageName!}.pojo.vo.${modelClass!}VO; +import ${packageName!}.pojo.excel.${modelClass!}Excel; +#if(hasWrapper) { +import ${packageName!}.wrapper.${modelClass!}Wrapper; +#} +import ${packageName!}.service.I${modelClass!}Service; +#if(isNotEmpty(superControllerClassPackage)){ +import ${superControllerClassPackage!}; +#} +#if(templateType=="tree"&&!hasWrapper){ +import ${packageName!}.wrapper.${modelClass!}Wrapper; +#} +import org.springblade.core.tool.utils.DateUtil; +import org.springblade.core.excel.util.ExcelUtil; +import org.springblade.core.tool.constant.BladeConstant; +import org.springblade.core.tool.constant.RoleConstant; +import java.util.Map; +import java.util.List; +import jakarta.servlet.http.HttpServletResponse; + +/** + * ${codeName!} 控制器 + * + * @author ${author!} + * @since ${date!} + */ +@RestController +@AllArgsConstructor +#if(hasServiceName) { +@RequestMapping("${serviceName!}/${modelCode!}") +#}else{ +@RequestMapping("/${modelCode!}") +#} +@Tag(name = "${codeName!}", description = "${codeName!}接口") +#if(isNotEmpty(superControllerClass)){ +public class ${modelClass!}Controller extends ${superControllerClass!} { +#} +#else{ +public class ${modelClass!}Controller { +#} + + private final I${modelClass!}Service ${modelCode!}Service; + +#if(hasWrapper){ + /** + * ${codeName!} 详情 + */ + @GetMapping("/detail") + @ApiOperationSupport(order = 1) + @Operation(summary = "详情", description = "传入${modelCode!}") + public R<${modelClass!}VO> detail(${modelClass!}Entity ${modelCode!}) { + ${modelClass!}Entity detail = ${modelCode!}Service.getOne(Condition.getQueryWrapper(${modelCode!})); + return R.data(${modelClass!}Wrapper.build().entityVO(detail)); + } +#if(templateType=="tree"){ + /** + * ${codeName!} 树列表 + */ + @GetMapping("/list") + @ApiOperationSupport(order = 2) + @Operation(summary = "分页", description = "传入${modelCode!}") + public R> list(@Parameter(hidden = true) @RequestParam Map ${modelCode!}, BladeUser bladeUser) { + QueryWrapper<${modelClass!}Entity> queryWrapper = Condition.getQueryWrapper(${modelCode!}, ${modelClass!}Entity.class); + List<${modelClass!}Entity> list = ${modelCode!}Service.list((!bladeUser.getTenantId().equals(BladeConstant.ADMIN_TENANT_ID)) ? queryWrapper.lambda().eq(${modelClass!}Entity::getTenantId, bladeUser.getTenantId()) : queryWrapper); + return R.data(${modelClass!}Wrapper.build().treeNodeVO(list)); + } +#}else{ + /** + * ${codeName!} 分页 + */ + @GetMapping("/list") + @ApiOperationSupport(order = 2) + @Operation(summary = "分页", description = "传入${modelCode!}") + public R> list(@Parameter(hidden = true) @RequestParam Map ${modelCode!}, Query query) { + IPage<${modelClass!}Entity> pages = ${modelCode!}Service.page(Condition.getPage(query), Condition.getQueryWrapper(${modelCode!}, ${modelClass!}Entity.class)); + return R.data(${modelClass!}Wrapper.build().pageVO(pages)); + } +#} +#}else{ + /** + * ${codeName!} 详情 + */ + @GetMapping("/detail") + @ApiOperationSupport(order = 1) + @Operation(summary = "详情", description = "传入${modelCode!}") + public R<${modelClass!}Entity> detail(${modelClass!}Entity ${modelCode!}) { + ${modelClass!}Entity detail = ${modelCode!}Service.getOne(Condition.getQueryWrapper(${modelCode!})); + return R.data(detail); + } +#if(templateType=="tree"){ + /** + * ${codeName!} 树列表 + */ + @GetMapping("/list") + @ApiOperationSupport(order = 2) + @Operation(summary = "分页", description = "传入notice") + public R> list(@Parameter(hidden = true) @RequestParam Map ${modelCode!}, BladeUser bladeUser) { + QueryWrapper<${modelClass!}Entity> queryWrapper = Condition.getQueryWrapper(${modelCode!}, ${modelClass!}Entity.class); + List<${modelClass!}Entity> list = ${modelCode!}Service.list((!bladeUser.getTenantId().equals(BladeConstant.ADMIN_TENANT_ID)) ? queryWrapper.lambda().eq(${modelClass!}Entity::getTenantId, bladeUser.getTenantId()) : queryWrapper); + return R.data(${modelClass!}Wrapper.build().treeNodeVO(list)); + } +#}else{ + /** + * ${codeName!} 分页 + */ + @GetMapping("/list") + @ApiOperationSupport(order = 2) + @Operation(summary = "分页", description = "传入${modelCode!}") + public R> list(@Parameter(hidden = true) @RequestParam Map ${modelCode!}, Query query) { + IPage<${modelClass!}Entity> pages = ${modelCode!}Service.page(Condition.getPage(query), Condition.getQueryWrapper(${modelCode!}, ${modelClass!}Entity.class)); + return R.data(pages); + } +#} +#} + + /** + * ${codeName!} 自定义分页 + */ + @GetMapping("/page") + @ApiOperationSupport(order = 3) + @Operation(summary = "分页", description = "传入${modelCode!}") + public R> page(${modelClass!}VO ${modelCode!}, Query query) { + IPage<${modelClass!}VO> pages = ${modelCode!}Service.select${modelClass!}Page(Condition.getPage(query), ${modelCode!}); + return R.data(pages); + } + + /** + * ${codeName!} 新增 + */ + @PostMapping("/save") + @ApiOperationSupport(order = 4) + @Operation(summary = "新增", description = "传入${modelCode!}") + public R save(@Valid @RequestBody ${modelClass!}Entity ${modelCode!}) { + return R.status(${modelCode!}Service.save(${modelCode!})); + } + + /** + * ${codeName!} 修改 + */ + @PostMapping("/update") + @ApiOperationSupport(order = 5) + @Operation(summary = "修改", description = "传入${modelCode!}") + public R update(@Valid @RequestBody ${modelClass!}Entity ${modelCode!}) { + return R.status(${modelCode!}Service.updateById(${modelCode!})); + } + + /** + * ${codeName!} 新增或修改 + */ + @PostMapping("/submit") + @ApiOperationSupport(order = 6) + @Operation(summary = "新增或修改", description = "传入${modelCode!}") + public R submit(@Valid @RequestBody ${modelClass!}Entity ${modelCode!}) { + return R.status(${modelCode!}Service.saveOrUpdate(${modelCode!})); + } + +#if(hasSuperEntity){ + /** + * ${codeName!} 删除 + */ + @PostMapping("/remove") + @ApiOperationSupport(order = 7) + @Operation(summary = "逻辑删除", description = "传入ids") + public R remove(@Parameter(name = "主键集合", required = true) @RequestParam String ids) { + return R.status(${modelCode!}Service.deleteLogic(Func.toLongList(ids))); + } +#}else{ + /** + * ${codeName!} 删除 + */ + @PostMapping("/remove") + @ApiOperationSupport(order = 7) + @Operation(summary = "删除", description = "传入ids") + public R remove(@Parameter(name = "主键集合", required = true) @RequestParam String ids) { + return R.status(${modelCode!}Service.removeByIds(Func.toLongList(ids))); + } +#} + +#if(templateType=="tree"){ + /** + * ${codeName!} 树形结构 + */ + @GetMapping("/tree") + @ApiOperationSupport(order = 8) + @Operation(summary = "树形结构", description = "树形结构") + public R> tree(String tenantId, BladeUser bladeUser) { + List<${modelClass!}VO> tree = ${modelCode!}Service.tree(Func.toStrWithEmpty(tenantId, bladeUser.getTenantId())); + return R.data(tree); + } +#} + + /** + * 导出数据 + */ + @PreAuth(RoleConstant.HAS_ROLE_ADMIN) + @GetMapping("/export-${modelCode!}") + @ApiOperationSupport(order = 9) + @Operation(summary = "导出数据", description = "传入${modelCode!}") + public void export${modelClass!}(@Parameter(hidden = true) @RequestParam Map ${modelCode!}, BladeUser bladeUser, HttpServletResponse response) { + QueryWrapper<${modelClass!}Entity> queryWrapper = Condition.getQueryWrapper(${modelCode!}, ${modelClass!}Entity.class); + //if (!AuthUtil.isAdministrator()) { + // queryWrapper.lambda().eq(${modelClass!}::getTenantId, bladeUser.getTenantId()); + //} + queryWrapper.lambda().eq(${modelClass!}Entity::getIsDeleted, BladeConstant.DB_NOT_DELETED); + List<${modelClass!}Excel> list = ${modelCode!}Service.export${modelClass!}(queryWrapper); + ExcelUtil.export(response, "${codeName!}数据" + DateUtil.time(), "${codeName!}数据表", list, ${modelClass!}Excel.class); + } + +} diff --git a/blade-starter-develop/src/main/resources/templates/api/entity.java.btl b/blade-starter-develop/src/main/resources/templates/api/entity.java.btl new file mode 100644 index 0000000..f061ce8 --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/api/entity.java.btl @@ -0,0 +1,92 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package ${package.Entity!}; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.media.Schema; +#for(x in propertyImport){ +import ${x!}; +#} +#if(hasSuperEntity){ +import lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; +#}else{ +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import java.io.Serializable; +#} + +/** + * ${codeName!} 实体类 + * + * @author ${author!} + * @since ${date!} + */ +@Data +@TableName("${model.modelTable!}") +@Schema(description = "${modelClass!}对象") +#if(hasSuperEntity){ +@EqualsAndHashCode(callSuper = true) +public class ${modelClass!}Entity extends TenantEntity { +#}else{ +public class ${modelClass!}Entity implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + /** + * 主键 + */ + @JsonSerialize(using = ToStringSerializer.class) + @Schema(description = "主键") + @TableId(value = "id", type = IdType.ASSIGN_ID) + private Long id; +#} + +#for(x in prototypes) { + #if(hasSuperEntity){ + #if(x.propertyName!="id"&&x.propertyName!="createUser"&&x.propertyName!="createDept"&&x.propertyName!="createTime"&&x.propertyName!="updateUser"&&x.propertyName!="updateTime"&&x.propertyName!="status"&&x.propertyName!="isDeleted"&&x.propertyName!="tenantId"){ + /** + * ${x.jdbcComment!} + */ + @Schema(description = "${x.jdbcComment!}") + private ${x.propertyType!} ${x.propertyName!}; + #} + #}else{ + #if(x.propertyName!="id"&&x.propertyName!="createUser"&&x.propertyName!="createDept"&&x.propertyName!="createTime"&&x.propertyName!="updateUser"&&x.propertyName!="updateTime"&&x.propertyName!="status"){ + /** + * ${x.jdbcComment!} + */ + @Schema(description = "${x.jdbcComment!}") + private ${x.propertyType!} ${x.propertyName!}; + #} + #} +#} + +} diff --git a/blade-starter-develop/src/main/resources/templates/api/entityDTO.java.btl b/blade-starter-develop/src/main/resources/templates/api/entityDTO.java.btl new file mode 100644 index 0000000..088a930 --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/api/entityDTO.java.btl @@ -0,0 +1,45 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package ${strutil.replace(package.Entity,"entity","dto")}; + +import ${packageName!}.pojo.entity.${modelClass!}Entity; +import lombok.Data; +import lombok.EqualsAndHashCode; +import java.io.Serial; + +/** + * ${codeName!} 数据传输对象实体类 + * + * @author ${author!} + * @since ${date!} + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class ${modelClass!}DTO extends ${modelClass!}Entity { + @Serial + private static final long serialVersionUID = 1L; + +} diff --git a/blade-starter-develop/src/main/resources/templates/api/entityExcel.java.btl b/blade-starter-develop/src/main/resources/templates/api/entityExcel.java.btl new file mode 100644 index 0000000..9117f90 --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/api/entityExcel.java.btl @@ -0,0 +1,68 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package ${strutil.replace(package.Entity,"pojo.entity","excel")}; + + +import lombok.Data; + +#for(x in propertyImport){ +import ${x!}; +#} +import com.alibaba.excel.annotation.ExcelProperty; +import com.alibaba.excel.annotation.write.style.ColumnWidth; +import com.alibaba.excel.annotation.write.style.ContentRowHeight; +import com.alibaba.excel.annotation.write.style.HeadRowHeight; +import java.io.Serializable; +import java.io.Serial; + + +/** + * ${codeName!} Excel实体类 + * + * @author ${author!} + * @since ${date!} + */ +@Data +@ColumnWidth(25) +@HeadRowHeight(20) +@ContentRowHeight(18) +public class ${modelClass!}Excel implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + +#for(x in prototypes) { + #if(x.propertyName!="id"&&x.propertyName!="createUser"&&x.propertyName!="createDept"&&x.propertyName!="createTime"&&x.propertyName!="updateUser"&&x.propertyName!="updateTime"&&x.propertyName!="status"){ + /** + * ${x.jdbcComment!} + */ + @ColumnWidth(20) + @ExcelProperty("${x.jdbcComment!}") + private ${x.propertyType!} ${x.propertyName!}; + #} +#} + +} diff --git a/blade-starter-develop/src/main/resources/templates/api/entityVO.java.btl b/blade-starter-develop/src/main/resources/templates/api/entityVO.java.btl new file mode 100644 index 0000000..c6fe31d --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/api/entityVO.java.btl @@ -0,0 +1,99 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package ${strutil.replace(package.Entity,"entity","vo")}; + +import ${packageName!}.pojo.entity.${modelClass!}Entity; +import org.springblade.core.tool.node.INode; +import lombok.Data; +import lombok.EqualsAndHashCode; +#if(templateType=="tree"){ +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; + +import java.util.ArrayList; +import java.util.List; +#} +import java.io.Serial; + +/** + * ${codeName!} 视图实体类 + * + * @author ${author!} + * @since ${date!} + */ +@Data +@EqualsAndHashCode(callSuper = true) +#if(templateType=="tree"){ +public class ${modelClass!}VO extends ${modelClass!}Entity implements INode<${modelClass!}VO> { + @Serial + private static final long serialVersionUID = 1L; + + /** + * 主键ID + */ + @JsonSerialize(using = ToStringSerializer.class) + private Long id; + + /** + * 父节点ID + */ + @JsonSerialize(using = ToStringSerializer.class) + private Long parentId; + + /** + * 父节点名称 + */ + private String parentName; + + /** + * 子孙节点 + */ + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private List<${modelClass!}VO> children; + + /** + * 是否有子孙节点 + */ + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private Boolean hasChildren; + + @Override + public List<${modelClass!}VO> getChildren() { + if (this.children == null) { + this.children = new ArrayList<>(); + } + return this.children; + } + +} +#}else{ +public class ${modelClass!}VO extends ${modelClass!}Entity { + @Serial + private static final long serialVersionUID = 1L; + +} +#} diff --git a/blade-starter-develop/src/main/resources/templates/api/feign.java.btl b/blade-starter-develop/src/main/resources/templates/api/feign.java.btl new file mode 100644 index 0000000..3ce2f20 --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/api/feign.java.btl @@ -0,0 +1,58 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package ${strutil.replace(package.Entity,"entity","feign")}; + +import org.springblade.core.mp.support.BladePage; +import ${packageName!}.pojo.entity.${modelClass!}Entity; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; + +/** + * ${codeName!} Feign接口类 + * + * @author ${author!} + * @since ${date!} + */ +@FeignClient( + value = "${serviceName!}" +) +public interface I${modelClass!}Client { + + String API_PREFIX = "/client"; + String TOP = API_PREFIX + "/top"; + + /** + * 获取${codeName!}列表 + * + * @param current 页号 + * @param size 页数 + * @return BladePage + */ + @GetMapping(TOP) + BladePage<${modelClass!}Entity> top(@RequestParam("current") Integer current, @RequestParam("size") Integer size); + +} diff --git a/blade-starter-develop/src/main/resources/templates/api/feignclient.java.btl b/blade-starter-develop/src/main/resources/templates/api/feignclient.java.btl new file mode 100644 index 0000000..aaa7ccf --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/api/feignclient.java.btl @@ -0,0 +1,61 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package ${strutil.replace(package.Entity,"entity","feign")}; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import lombok.AllArgsConstructor; +import org.springblade.core.mp.support.BladePage; +import org.springblade.core.mp.support.Condition; +import org.springblade.core.mp.support.Query; +import ${packageName!}.pojo.entity.${modelClass!}Entity; +import ${packageName!}.service.I${modelClass!}Service; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * ${codeName!} Feign实现类 + * + * @author ${author!} + * @since ${date!} + */ +@Hidden() +@RestController +@AllArgsConstructor +public class ${modelClass!}Client implements I${modelClass!}Client { + + private final I${modelClass!}Service ${modelCode!}Service; + + @Override + @GetMapping(TOP) + public BladePage<${modelClass!}Entity> top(Integer current, Integer size) { + Query query = new Query(); + query.setCurrent(current); + query.setSize(size); + IPage<${modelClass!}Entity> page = service.page(Condition.getPage(query)); + return BladePage.of(page); + } + +} diff --git a/blade-starter-develop/src/main/resources/templates/api/mapper.java.btl b/blade-starter-develop/src/main/resources/templates/api/mapper.java.btl new file mode 100644 index 0000000..39f1664 --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/api/mapper.java.btl @@ -0,0 +1,72 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package ${package.Mapper!}; + +import ${packageName!}.pojo.entity.${modelClass!}Entity; +import ${packageName!}.pojo.vo.${modelClass!}VO; +import ${packageName!}.pojo.excel.${modelClass!}Excel; +import ${superMapperClassPackage!}; +import com.baomidou.mybatisplus.core.conditions.Wrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import org.apache.ibatis.annotations.Param; +import java.util.List; + +/** + * ${codeName!} Mapper 接口 + * + * @author ${author!} + * @since ${date!} + */ +public interface ${modelClass!}Mapper extends ${superMapperClass!}<${modelClass!}Entity> { + + /** + * 自定义分页 + * + * @param page + * @param ${modelCode!} + * @return + */ + List<${modelClass!}VO> select${modelClass!}Page(IPage page, ${modelClass!}VO ${modelCode!}); + +#if(templateType=="tree"){ + /** + * 获取树形节点 + * + * @param tenantId + * @return + */ + List<${modelClass!}VO> tree(String tenantId); +#} + + /** + * 获取导出数据 + * + * @param queryWrapper + * @return + */ + List<${modelClass!}Excel> export${modelClass!}(@Param("ew") Wrapper<${modelClass!}Entity> queryWrapper); + +} diff --git a/blade-starter-develop/src/main/resources/templates/api/mapper.xml.btl b/blade-starter-develop/src/main/resources/templates/api/mapper.xml.btl new file mode 100644 index 0000000..88d63b0 --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/api/mapper.xml.btl @@ -0,0 +1,43 @@ + + + + +#if(enableCache){ + + +#} + + + #for(x in prototypes) { + + #} + + +#if(templateType=="tree"){ + + + + + + + +#} + + + +#if(templateType=="tree"){ + +#} + + + + diff --git a/blade-starter-develop/src/main/resources/templates/api/service.java.btl b/blade-starter-develop/src/main/resources/templates/api/service.java.btl new file mode 100644 index 0000000..b99d0e7 --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/api/service.java.btl @@ -0,0 +1,78 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package ${package.Service!}; + +import com.baomidou.mybatisplus.core.conditions.Wrapper; +import ${packageName!}.pojo.entity.${modelClass!}Entity; +import ${packageName!}.pojo.vo.${modelClass!}VO; +import ${packageName!}.pojo.excel.${modelClass!}Excel; +import com.baomidou.mybatisplus.core.metadata.IPage; +#if(hasSuperEntity){ +import ${superServiceClassPackage!}; +#}else{ +import com.baomidou.mybatisplus.extension.service.IService; +#} +import java.util.List; + +/** + * ${codeName!} 服务类 + * + * @author ${author!} + * @since ${date!} + */ +#if(hasSuperEntity){ +public interface I${modelClass!}Service extends ${superServiceClass!}<${modelClass!}Entity> { +#}else{ +public interface I${modelClass!}Service extends IService<${modelClass!}Entity> { +#} + /** + * 自定义分页 + * + * @param page + * @param ${modelCode!} + * @return + */ + IPage<${modelClass!}VO> select${modelClass!}Page(IPage<${modelClass!}VO> page, ${modelClass!}VO ${modelCode!}); + +#if(templateType=="tree"){ + /** + * 树形结构 + * + * @param tenantId + * @return + */ + List<${modelClass!}VO> tree(String tenantId); +#} + + /** + * 导出数据 + * + * @param queryWrapper + * @return + */ + List<${modelClass!}Excel> export${modelClass!}(Wrapper<${modelClass!}Entity> queryWrapper); + +} diff --git a/blade-starter-develop/src/main/resources/templates/api/serviceImpl.java.btl b/blade-starter-develop/src/main/resources/templates/api/serviceImpl.java.btl new file mode 100644 index 0000000..92beeff --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/api/serviceImpl.java.btl @@ -0,0 +1,80 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package ${package.ServiceImpl!}; + +import ${packageName!}.pojo.entity.${modelClass!}Entity; +import ${packageName!}.pojo.vo.${modelClass!}VO; +import ${packageName!}.pojo.excel.${modelClass!}Excel; +import ${packageName!}.mapper.${model.modelClass!}Mapper; +import ${packageName!}.service.I${model.modelClass!}Service; +import org.springframework.stereotype.Service; +import com.baomidou.mybatisplus.core.conditions.Wrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +#if(templateType=="tree"){ +import org.springblade.core.tool.node.ForestNodeMerger; +#} +#if(hasSuperEntity){ +import ${superServiceImplClassPackage!}; +#}else{ +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +#} +import java.util.List; + +/** + * ${codeName!} 服务实现类 + * + * @author ${author!} + * @since ${date!} + */ +@Service +#if(hasSuperEntity){ +public class ${modelClass!}ServiceImpl extends ${superServiceImplClass!}<${modelClass!}Mapper, ${modelClass!}Entity> implements I${model.modelClass!}Service { +#}else{ +public class ${modelClass!}ServiceImpl extends ServiceImpl<${modelClass!}Mapper, ${modelClass!}Entity> implements I${model.modelClass!}Service { +#} + + @Override + public IPage<${modelClass!}VO> select${modelClass!}Page(IPage<${modelClass!}VO> page, ${modelClass!}VO ${modelCode!}) { + return page.setRecords(baseMapper.select${modelClass!}Page(page, ${modelCode!})); + } + +#if(templateType=="tree"){ + @Override + public List<${modelClass!}VO> tree(String tenantId) { + return ForestNodeMerger.merge(baseMapper.tree(tenantId)); + } +#} + + @Override + public List<${modelClass!}Excel> export${modelClass!}(Wrapper<${modelClass!}Entity> queryWrapper) { + List<${modelClass!}Excel> ${modelCode!}List = baseMapper.export${modelClass!}(queryWrapper); + //${modelCode!}List.forEach(${modelCode!} -> { + // ${modelCode!}.setTypeName(DictCache.getValue(DictEnum.YES_NO, ${modelClass!}.getType())); + //}); + return ${modelCode!}List; + } + +} diff --git a/blade-starter-develop/src/main/resources/templates/api/wrapper.java.btl b/blade-starter-develop/src/main/resources/templates/api/wrapper.java.btl new file mode 100644 index 0000000..ed23bb6 --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/api/wrapper.java.btl @@ -0,0 +1,70 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package ${strutil.replace(package.Entity,"pojo.entity","wrapper")}; + +import org.springblade.core.mp.support.BaseEntityWrapper; +import org.springblade.core.tool.utils.BeanUtil; +import ${packageName!}.pojo.entity.${modelClass!}Entity; +import ${packageName!}.pojo.vo.${modelClass!}VO; +import java.util.Objects; +#if(templateType=="tree"){ +import org.springblade.core.tool.node.ForestNodeMerger; +import java.util.List; +import java.util.stream.Collectors; +#} + +/** + * ${codeName!} 包装类,返回视图层所需的字段 + * + * @author ${author!} + * @since ${date!} + */ +public class ${modelClass!}Wrapper extends BaseEntityWrapper<${modelClass!}Entity, ${modelClass!}VO> { + + public static ${modelClass!}Wrapper build() { + return new ${modelClass!}Wrapper(); + } + + @Override + public ${modelClass!}VO entityVO(${modelClass!}Entity ${modelCode!}) { + ${modelClass!}VO ${modelCode!}VO = Objects.requireNonNull(BeanUtil.copyProperties(${modelCode!}, ${modelClass!}VO.class)); + + //User createUser = UserCache.getUser(${modelCode!}.getCreateUser()); + //User updateUser = UserCache.getUser(${modelCode!}.getUpdateUser()); + //${modelCode!}VO.setCreateUserName(createUser.getName()); + //${modelCode!}VO.setUpdateUserName(updateUser.getName()); + + return ${modelCode!}VO; + } + +#if(templateType=="tree"){ + public List<${modelClass!}VO> treeNodeVO(List<${modelClass!}Entity> list) { + List<${modelClass!}VO> collect = list.stream().map(${modelCode!} -> BeanUtil.copyProperties(${modelCode!}, ${modelClass!}VO.class)).collect(Collectors.toList()); + return ForestNodeMerger.merge(collect); + } +#} + +} diff --git a/blade-starter-develop/src/main/resources/templates/code.properties b/blade-starter-develop/src/main/resources/templates/code.properties new file mode 100644 index 0000000..50420cb --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/code.properties @@ -0,0 +1,5 @@ +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver +spring.datasource.url=jdbc:mysql://localhost:3306/bladex?useSSL=false&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&transformedBitIsBoolean=true&tinyInt1isBit=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true +spring.datasource.username=root +spring.datasource.password=root +author=BladeX diff --git a/blade-starter-develop/src/main/resources/templates/element-plus/crud/api.js.btl b/blade-starter-develop/src/main/resources/templates/element-plus/crud/api.js.btl new file mode 100644 index 0000000..db6dabe --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/element-plus/crud/api.js.btl @@ -0,0 +1,50 @@ +import request from '@/axios'; + +export const getList = (current, size, params) => { + return request({ + url: '/${serviceName!}/${modelCode!}/list', + method: 'get', + params: { + ...params, + current, + size, + } + }) +} + +export const getDetail = (id) => { + return request({ + url: '/${serviceName!}/${modelCode!}/detail', + method: 'get', + params: { + id + } + }) +} + +export const remove = (ids) => { + return request({ + url: '/${serviceName!}/${modelCode!}/remove', + method: 'post', + params: { + ids, + } + }) +} + +export const add = (row) => { + return request({ + url: '/${serviceName!}/${modelCode!}/submit', + method: 'post', + data: row + }) +} + +export const update = (row) => { + return request({ + url: '/${serviceName!}/${modelCode!}/submit', + method: 'post', + data: row + }) +} + diff --git a/blade-starter-develop/src/main/resources/templates/element-plus/crud/crud.vue.btl b/blade-starter-develop/src/main/resources/templates/element-plus/crud/crud.vue.btl new file mode 100644 index 0000000..6b35ff8 --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/element-plus/crud/crud.vue.btl @@ -0,0 +1,386 @@ + + + diff --git a/blade-starter-develop/src/main/resources/templates/element-plus/crud/option.js.btl b/blade-starter-develop/src/main/resources/templates/element-plus/crud/option.js.btl new file mode 100644 index 0000000..41cc99d --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/element-plus/crud/option.js.btl @@ -0,0 +1,30 @@ +export default { + expand: false, + index: true, + border: true, + selection: true, + column: [ +#for(x in prototypes) { + { + label: "${x.jdbcComment!}", + prop: "${x.propertyName!}", +#if(strutil.contain(x.componentType,"date")||strutil.contain(x.componentType,"time")){ + format: "YYYY-MM-DD HH:mm:ss", + valueFormat: "YYYY-MM-DD HH:mm:ss", +#} +#if(x.isForm==0){ + display: false, +#} +#if(x.isRow==1){ + span: 24, +#} +#if(x.isList==0){ + hide: true, +#} +#if(x.isQuery==1){ + search: true, +#} + }, +#} + ] +} diff --git a/blade-starter-develop/src/main/resources/templates/element-plus/sub/api.js.btl b/blade-starter-develop/src/main/resources/templates/element-plus/sub/api.js.btl new file mode 100644 index 0000000..db6dabe --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/element-plus/sub/api.js.btl @@ -0,0 +1,50 @@ +import request from '@/axios'; + +export const getList = (current, size, params) => { + return request({ + url: '/${serviceName!}/${modelCode!}/list', + method: 'get', + params: { + ...params, + current, + size, + } + }) +} + +export const getDetail = (id) => { + return request({ + url: '/${serviceName!}/${modelCode!}/detail', + method: 'get', + params: { + id + } + }) +} + +export const remove = (ids) => { + return request({ + url: '/${serviceName!}/${modelCode!}/remove', + method: 'post', + params: { + ids, + } + }) +} + +export const add = (row) => { + return request({ + url: '/${serviceName!}/${modelCode!}/submit', + method: 'post', + data: row + }) +} + +export const update = (row) => { + return request({ + url: '/${serviceName!}/${modelCode!}/submit', + method: 'post', + data: row + }) +} + diff --git a/blade-starter-develop/src/main/resources/templates/element-plus/sub/crud.vue.btl b/blade-starter-develop/src/main/resources/templates/element-plus/sub/crud.vue.btl new file mode 100644 index 0000000..d92008c --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/element-plus/sub/crud.vue.btl @@ -0,0 +1,414 @@ + + + diff --git a/blade-starter-develop/src/main/resources/templates/element-plus/sub/option.js.btl b/blade-starter-develop/src/main/resources/templates/element-plus/sub/option.js.btl new file mode 100644 index 0000000..41cc99d --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/element-plus/sub/option.js.btl @@ -0,0 +1,30 @@ +export default { + expand: false, + index: true, + border: true, + selection: true, + column: [ +#for(x in prototypes) { + { + label: "${x.jdbcComment!}", + prop: "${x.propertyName!}", +#if(strutil.contain(x.componentType,"date")||strutil.contain(x.componentType,"time")){ + format: "YYYY-MM-DD HH:mm:ss", + valueFormat: "YYYY-MM-DD HH:mm:ss", +#} +#if(x.isForm==0){ + display: false, +#} +#if(x.isRow==1){ + span: 24, +#} +#if(x.isList==0){ + hide: true, +#} +#if(x.isQuery==1){ + search: true, +#} + }, +#} + ] +} diff --git a/blade-starter-develop/src/main/resources/templates/element-plus/sub/sub.vue.btl b/blade-starter-develop/src/main/resources/templates/element-plus/sub/sub.vue.btl new file mode 100644 index 0000000..b2d72b6 --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/element-plus/sub/sub.vue.btl @@ -0,0 +1,372 @@ + + + diff --git a/blade-starter-develop/src/main/resources/templates/element-plus/tree/api.js.btl b/blade-starter-develop/src/main/resources/templates/element-plus/tree/api.js.btl new file mode 100644 index 0000000..1965fb5 --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/element-plus/tree/api.js.btl @@ -0,0 +1,60 @@ +import request from '@/axios'; + +export const getList = (current, size, params) => { + return request({ + url: '/${serviceName!}/${modelCode!}/list', + method: 'get', + params: { + ...params, + current, + size, + } + }) +} + +export const getDetail = (id) => { + return request({ + url: '/${serviceName!}/${modelCode!}/detail', + method: 'get', + params: { + id + } + }) +} + +export const getTree = (tenantId) => { + return request({ + url: '/${serviceName!}/${modelCode!}/tree', + method: 'get', + params: { + tenantId, + } + }) +} + +export const remove = (ids) => { + return request({ + url: '/${serviceName!}/${modelCode!}/remove', + method: 'post', + params: { + ids, + } + }) +} + +export const add = (row) => { + return request({ + url: '/${serviceName!}/${modelCode!}/submit', + method: 'post', + data: row + }) +} + +export const update = (row) => { + return request({ + url: '/${serviceName!}/${modelCode!}/submit', + method: 'post', + data: row + }) +} + diff --git a/blade-starter-develop/src/main/resources/templates/element-plus/tree/crud.vue.btl b/blade-starter-develop/src/main/resources/templates/element-plus/tree/crud.vue.btl new file mode 100644 index 0000000..0e8907a --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/element-plus/tree/crud.vue.btl @@ -0,0 +1,412 @@ + + + diff --git a/blade-starter-develop/src/main/resources/templates/element-plus/tree/option.js.btl b/blade-starter-develop/src/main/resources/templates/element-plus/tree/option.js.btl new file mode 100644 index 0000000..41cc99d --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/element-plus/tree/option.js.btl @@ -0,0 +1,30 @@ +export default { + expand: false, + index: true, + border: true, + selection: true, + column: [ +#for(x in prototypes) { + { + label: "${x.jdbcComment!}", + prop: "${x.propertyName!}", +#if(strutil.contain(x.componentType,"date")||strutil.contain(x.componentType,"time")){ + format: "YYYY-MM-DD HH:mm:ss", + valueFormat: "YYYY-MM-DD HH:mm:ss", +#} +#if(x.isForm==0){ + display: false, +#} +#if(x.isRow==1){ + span: 24, +#} +#if(x.isList==0){ + hide: true, +#} +#if(x.isQuery==1){ + search: true, +#} + }, +#} + ] +} diff --git a/blade-starter-develop/src/main/resources/templates/element/crud/api.js.btl b/blade-starter-develop/src/main/resources/templates/element/crud/api.js.btl new file mode 100644 index 0000000..fa12324 --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/element/crud/api.js.btl @@ -0,0 +1,50 @@ +import request from '@/router/axios'; + +export const getList = (current, size, params) => { + return request({ + url: '/api/${serviceName!}/${modelCode!}/list', + method: 'get', + params: { + ...params, + current, + size, + } + }) +} + +export const getDetail = (id) => { + return request({ + url: '/api/${serviceName!}/${modelCode!}/detail', + method: 'get', + params: { + id + } + }) +} + +export const remove = (ids) => { + return request({ + url: '/api/${serviceName!}/${modelCode!}/remove', + method: 'post', + params: { + ids, + } + }) +} + +export const add = (row) => { + return request({ + url: '/api/${serviceName!}/${modelCode!}/submit', + method: 'post', + data: row + }) +} + +export const update = (row) => { + return request({ + url: '/api/${serviceName!}/${modelCode!}/submit', + method: 'post', + data: row + }) +} + diff --git a/blade-starter-develop/src/main/resources/templates/element/crud/crud.vue.btl b/blade-starter-develop/src/main/resources/templates/element/crud/crud.vue.btl new file mode 100644 index 0000000..c9a2538 --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/element/crud/crud.vue.btl @@ -0,0 +1,371 @@ + + + + + diff --git a/blade-starter-develop/src/main/resources/templates/element/crud/option.js.btl b/blade-starter-develop/src/main/resources/templates/element/crud/option.js.btl new file mode 100644 index 0000000..0ca18f9 --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/element/crud/option.js.btl @@ -0,0 +1,31 @@ +export default { + size: 'small', + expand: false, + index: true, + border: true, + selection: true, + column: [ +#for(x in prototypes) { + { + label: "${x.jdbcComment!}", + prop: "${x.propertyName!}", +#if(strutil.contain(x.componentType,"date")||strutil.contain(x.componentType,"time")){ + format: "yyyy-MM-dd hh:mm:ss", + valueFormat: "yyyy-MM-dd hh:mm:ss", +#} +#if(x.isForm==0){ + display: false, +#} +#if(x.isRow==1){ + span: 24, +#} +#if(x.isList==0){ + hide: true, +#} +#if(x.isQuery==1){ + search: true, +#} + }, +#} + ] +} diff --git a/blade-starter-develop/src/main/resources/templates/element/sub/api.js.btl b/blade-starter-develop/src/main/resources/templates/element/sub/api.js.btl new file mode 100644 index 0000000..fa12324 --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/element/sub/api.js.btl @@ -0,0 +1,50 @@ +import request from '@/router/axios'; + +export const getList = (current, size, params) => { + return request({ + url: '/api/${serviceName!}/${modelCode!}/list', + method: 'get', + params: { + ...params, + current, + size, + } + }) +} + +export const getDetail = (id) => { + return request({ + url: '/api/${serviceName!}/${modelCode!}/detail', + method: 'get', + params: { + id + } + }) +} + +export const remove = (ids) => { + return request({ + url: '/api/${serviceName!}/${modelCode!}/remove', + method: 'post', + params: { + ids, + } + }) +} + +export const add = (row) => { + return request({ + url: '/api/${serviceName!}/${modelCode!}/submit', + method: 'post', + data: row + }) +} + +export const update = (row) => { + return request({ + url: '/api/${serviceName!}/${modelCode!}/submit', + method: 'post', + data: row + }) +} + diff --git a/blade-starter-develop/src/main/resources/templates/element/sub/crud.vue.btl b/blade-starter-develop/src/main/resources/templates/element/sub/crud.vue.btl new file mode 100644 index 0000000..c81aaa1 --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/element/sub/crud.vue.btl @@ -0,0 +1,399 @@ + + + + + diff --git a/blade-starter-develop/src/main/resources/templates/element/sub/option.js.btl b/blade-starter-develop/src/main/resources/templates/element/sub/option.js.btl new file mode 100644 index 0000000..0ca18f9 --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/element/sub/option.js.btl @@ -0,0 +1,31 @@ +export default { + size: 'small', + expand: false, + index: true, + border: true, + selection: true, + column: [ +#for(x in prototypes) { + { + label: "${x.jdbcComment!}", + prop: "${x.propertyName!}", +#if(strutil.contain(x.componentType,"date")||strutil.contain(x.componentType,"time")){ + format: "yyyy-MM-dd hh:mm:ss", + valueFormat: "yyyy-MM-dd hh:mm:ss", +#} +#if(x.isForm==0){ + display: false, +#} +#if(x.isRow==1){ + span: 24, +#} +#if(x.isList==0){ + hide: true, +#} +#if(x.isQuery==1){ + search: true, +#} + }, +#} + ] +} diff --git a/blade-starter-develop/src/main/resources/templates/element/sub/sub.vue.btl b/blade-starter-develop/src/main/resources/templates/element/sub/sub.vue.btl new file mode 100644 index 0000000..3dd0f25 --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/element/sub/sub.vue.btl @@ -0,0 +1,358 @@ + + + + + diff --git a/blade-starter-develop/src/main/resources/templates/element/tree/api.js.btl b/blade-starter-develop/src/main/resources/templates/element/tree/api.js.btl new file mode 100644 index 0000000..ff4186a --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/element/tree/api.js.btl @@ -0,0 +1,60 @@ +import request from '@/router/axios'; + +export const getList = (current, size, params) => { + return request({ + url: '/api/${serviceName!}/${modelCode!}/list', + method: 'get', + params: { + ...params, + current, + size, + } + }) +} + +export const getDetail = (id) => { + return request({ + url: '/api/${serviceName!}/${modelCode!}/detail', + method: 'get', + params: { + id + } + }) +} + +export const getTree = (tenantId) => { + return request({ + url: '/api/${serviceName!}/${modelCode!}/tree', + method: 'get', + params: { + tenantId, + } + }) +} + +export const remove = (ids) => { + return request({ + url: '/api/${serviceName!}/${modelCode!}/remove', + method: 'post', + params: { + ids, + } + }) +} + +export const add = (row) => { + return request({ + url: '/api/${serviceName!}/${modelCode!}/submit', + method: 'post', + data: row + }) +} + +export const update = (row) => { + return request({ + url: '/api/${serviceName!}/${modelCode!}/submit', + method: 'post', + data: row + }) +} + diff --git a/blade-starter-develop/src/main/resources/templates/element/tree/crud.vue.btl b/blade-starter-develop/src/main/resources/templates/element/tree/crud.vue.btl new file mode 100644 index 0000000..f6b38c2 --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/element/tree/crud.vue.btl @@ -0,0 +1,376 @@ + + + + + diff --git a/blade-starter-develop/src/main/resources/templates/element/tree/option.js.btl b/blade-starter-develop/src/main/resources/templates/element/tree/option.js.btl new file mode 100644 index 0000000..0ca18f9 --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/element/tree/option.js.btl @@ -0,0 +1,31 @@ +export default { + size: 'small', + expand: false, + index: true, + border: true, + selection: true, + column: [ +#for(x in prototypes) { + { + label: "${x.jdbcComment!}", + prop: "${x.propertyName!}", +#if(strutil.contain(x.componentType,"date")||strutil.contain(x.componentType,"time")){ + format: "yyyy-MM-dd hh:mm:ss", + valueFormat: "yyyy-MM-dd hh:mm:ss", +#} +#if(x.isForm==0){ + display: false, +#} +#if(x.isRow==1){ + span: 24, +#} +#if(x.isList==0){ + hide: true, +#} +#if(x.isQuery==1){ + search: true, +#} + }, +#} + ] +} diff --git a/blade-starter-develop/src/main/resources/templates/lemon/crud/Modal.vue.btl b/blade-starter-develop/src/main/resources/templates/lemon/crud/Modal.vue.btl new file mode 100644 index 0000000..40438c1 --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/lemon/crud/Modal.vue.btl @@ -0,0 +1,90 @@ + + diff --git a/blade-starter-develop/src/main/resources/templates/lemon/crud/data.data.ts.btl b/blade-starter-develop/src/main/resources/templates/lemon/crud/data.data.ts.btl new file mode 100644 index 0000000..5ad1bb8 --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/lemon/crud/data.data.ts.btl @@ -0,0 +1,102 @@ +import { BasicColumn } from '@/components/Table'; +import { FormSchema } from '@/components/Table'; +import { DescItem } from '@/components/Description/index'; +import { getDictList } from '@/api/system/system'; + + +export const columns: BasicColumn[] = [ + #for(x in prototypes) { + #if(x.isList==1){ + { + title: "${x.jdbcComment!}", + dataIndex: "${x.propertyName!}", + }, + #} + #} + ]; + +export const searchFormSchema: FormSchema[] = [ + #for(x in prototypes) { + #if(x.isQuery==1){ + { + field: "${x.propertyName!}_${x.queryType!}", + label: "${x.jdbcComment!}", + #if(x.componentType=="input"){ + component: 'Input', + #}else if(x.componentType=="textarea"){ + component: 'InputTextArea', + #}else if(x.componentType=="select"){ + component: 'ApiSelect', + #}else if(x.componentType=="tree"){ + component: 'ApiTreeSelect', + #}else if(x.componentType=="radio"){ + component: 'RadioGroup', + #}else if(x.componentType=="checkbox"){ + component: 'Checkbox', + #}else if(x.componentType=="switch"){ + component: 'Switch', + #}else if(x.componentType=="date"){ + component: 'DatePicker', + #} + #if(x.componentType=="select"&&x.dictCode!=null){ + componentProps: { + api: getDictList, + params: { code: '${x.dictCode!}' }, + labelField: 'dictValue', + valueField: 'dictKey', + }, + #} + }, + #} + #} +]; + +export const formSchema: FormSchema[] = [ + #for(x in prototypes) { + #if(x.isForm!=0){ + { + field: "${x.propertyName!}", + label: "${x.jdbcComment!}", + #if(x.componentType=="input"){ + component: 'Input', + #}else if(x.componentType=="textarea"){ + component: 'InputTextArea', + #}else if(x.componentType=="select"){ + component: 'ApiSelect', + #}else if(x.componentType=="tree"){ + component: 'ApiTreeSelect', + #}else if(x.componentType=="radio"){ + component: 'RadioGroup', + #}else if(x.componentType=="checkbox"){ + component: 'Checkbox', + #}else if(x.componentType=="switch"){ + component: 'Switch', + #}else if(x.componentType=="date"){ + component: 'DatePicker', + #} + #if(x.componentType=="select"&&x.dictCode!=null){ + componentProps: { + api: getDictList, + params: { code: '${x.dictCode!}' }, + labelField: 'dictValue', + valueField: 'dictKey', + }, + #} + #if(x.isRequired==1){ + required: true, + #} + }, + #} + #} +]; + +export const detailSchema: DescItem[] = [ + #for(x in prototypes) { + #if(x.isForm!=0){ + { + field: "${x.propertyName!}", + label: "${x.jdbcComment!}", + }, + #} + #} +]; diff --git a/blade-starter-develop/src/main/resources/templates/lemon/crud/data.ts.btl b/blade-starter-develop/src/main/resources/templates/lemon/crud/data.ts.btl new file mode 100644 index 0000000..9083b21 --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/lemon/crud/data.ts.btl @@ -0,0 +1,30 @@ +import { defHttp } from '@/utils/http/axios'; + +enum Api { + List = '/${serviceName!}/${modelCode!}/list', + Submit = '/${serviceName!}/${modelCode!}/submit', + Detail = '/${serviceName!}/${modelCode!}/detail', + Remove = '/${serviceName!}/${modelCode!}/remove', +} + +//列表 +export function getList(params?: object) { + return defHttp.get({ url: Api.List, params: params }, + { joinParamsToUrl: true, joinTime: false }); +} + +//提交 +export function submitObj(params?: object) { + return defHttp.post({ url: Api.Submit, params: params }); +} + +//详情 +export function getDetail(params?: object) { + return defHttp.get({ url: Api.Detail, params }); +} + +//删除 +export function remove(params?: object) { + return defHttp.post({ url: Api.Remove, params }, { joinParamsToUrl: true }); +} + diff --git a/blade-starter-develop/src/main/resources/templates/lemon/crud/index.vue.btl b/blade-starter-develop/src/main/resources/templates/lemon/crud/index.vue.btl new file mode 100644 index 0000000..1e1646c --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/lemon/crud/index.vue.btl @@ -0,0 +1,124 @@ + + diff --git a/blade-starter-develop/src/main/resources/templates/lemon/sub/Modal.vue.btl b/blade-starter-develop/src/main/resources/templates/lemon/sub/Modal.vue.btl new file mode 100644 index 0000000..40438c1 --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/lemon/sub/Modal.vue.btl @@ -0,0 +1,90 @@ + + diff --git a/blade-starter-develop/src/main/resources/templates/lemon/sub/data.data.ts.btl b/blade-starter-develop/src/main/resources/templates/lemon/sub/data.data.ts.btl new file mode 100644 index 0000000..ee6b555 --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/lemon/sub/data.data.ts.btl @@ -0,0 +1,102 @@ +import { BasicColumn } from '@/components/Table'; +import { FormSchema } from '@/components/Table'; +import { DescItem } from '@/components/Description/index'; +import { getDictList } from '@/api/system/system'; + + +export const columns: BasicColumn[] = [ + #for(x in prototypes) { + #if(x.isList==1){ + { + title: "${x.jdbcComment!}", + dataIndex: "${x.propertyName!}", + }, + #} + #} + ]; + +export const searchFormSchema: FormSchema[] = [ + #for(x in prototypes) { + #if(x.isQuery==1){ + { + field: "${x.propertyName!}", + label: "${x.jdbcComment!}", + #if(x.componentType=="input"){ + component: 'Input', + #}else if(x.componentType=="textarea"){ + component: 'InputTextArea', + #}else if(x.componentType=="select"){ + component: 'ApiSelect', + #}else if(x.componentType=="tree"){ + component: 'ApiTreeSelect', + #}else if(x.componentType=="radio"){ + component: 'RadioGroup', + #}else if(x.componentType=="checkbox"){ + component: 'Checkbox', + #}else if(x.componentType=="switch"){ + component: 'Switch', + #}else if(x.componentType=="date"){ + component: 'DatePicker', + #} + #if(x.componentType=="select"&&x.dictCode!=null){ + componentProps: { + api: getDictList, + params: { code: '${x.dictCode!}' }, + labelField: 'dictValue', + valueField: 'dictKey', + }, + #} + }, + #} + #} +]; + +export const formSchema: FormSchema[] = [ + #for(x in prototypes) { + #if(x.isForm!=0){ + { + field: "${x.propertyName!}", + label: "${x.jdbcComment!}", + #if(x.componentType=="input"){ + component: 'Input', + #}else if(x.componentType=="textarea"){ + component: 'InputTextArea', + #}else if(x.componentType=="select"){ + component: 'ApiSelect', + #}else if(x.componentType=="tree"){ + component: 'ApiTreeSelect', + #}else if(x.componentType=="radio"){ + component: 'RadioGroup', + #}else if(x.componentType=="checkbox"){ + component: 'Checkbox', + #}else if(x.componentType=="switch"){ + component: 'Switch', + #}else if(x.componentType=="date"){ + component: 'DatePicker', + #} + #if(x.componentType=="select"&&x.dictCode!=null){ + componentProps: { + api: getDictList, + params: { code: '${x.dictCode!}' }, + labelField: 'dictValue', + valueField: 'dictKey', + }, + #} + #if(x.isRequired==1){ + required: true, + #} + }, + #} + #} +]; + +export const detailSchema: DescItem[] = [ + #for(x in prototypes) { + #if(x.isForm!=0){ + { + field: "${x.propertyName!}", + label: "${x.jdbcComment!}", + }, + #} + #} +]; diff --git a/blade-starter-develop/src/main/resources/templates/lemon/sub/data.ts.btl b/blade-starter-develop/src/main/resources/templates/lemon/sub/data.ts.btl new file mode 100644 index 0000000..9083b21 --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/lemon/sub/data.ts.btl @@ -0,0 +1,30 @@ +import { defHttp } from '@/utils/http/axios'; + +enum Api { + List = '/${serviceName!}/${modelCode!}/list', + Submit = '/${serviceName!}/${modelCode!}/submit', + Detail = '/${serviceName!}/${modelCode!}/detail', + Remove = '/${serviceName!}/${modelCode!}/remove', +} + +//列表 +export function getList(params?: object) { + return defHttp.get({ url: Api.List, params: params }, + { joinParamsToUrl: true, joinTime: false }); +} + +//提交 +export function submitObj(params?: object) { + return defHttp.post({ url: Api.Submit, params: params }); +} + +//详情 +export function getDetail(params?: object) { + return defHttp.get({ url: Api.Detail, params }); +} + +//删除 +export function remove(params?: object) { + return defHttp.post({ url: Api.Remove, params }, { joinParamsToUrl: true }); +} + diff --git a/blade-starter-develop/src/main/resources/templates/lemon/sub/index.vue.btl b/blade-starter-develop/src/main/resources/templates/lemon/sub/index.vue.btl new file mode 100644 index 0000000..ad81697 --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/lemon/sub/index.vue.btl @@ -0,0 +1,143 @@ + + diff --git a/blade-starter-develop/src/main/resources/templates/lemon/sub/sub.vue.btl b/blade-starter-develop/src/main/resources/templates/lemon/sub/sub.vue.btl new file mode 100644 index 0000000..5806c30 --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/lemon/sub/sub.vue.btl @@ -0,0 +1,113 @@ + + diff --git a/blade-starter-develop/src/main/resources/templates/lemon/tree/Modal.vue.btl b/blade-starter-develop/src/main/resources/templates/lemon/tree/Modal.vue.btl new file mode 100644 index 0000000..40438c1 --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/lemon/tree/Modal.vue.btl @@ -0,0 +1,90 @@ + + diff --git a/blade-starter-develop/src/main/resources/templates/lemon/tree/data.data.ts.btl b/blade-starter-develop/src/main/resources/templates/lemon/tree/data.data.ts.btl new file mode 100644 index 0000000..ee6b555 --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/lemon/tree/data.data.ts.btl @@ -0,0 +1,102 @@ +import { BasicColumn } from '@/components/Table'; +import { FormSchema } from '@/components/Table'; +import { DescItem } from '@/components/Description/index'; +import { getDictList } from '@/api/system/system'; + + +export const columns: BasicColumn[] = [ + #for(x in prototypes) { + #if(x.isList==1){ + { + title: "${x.jdbcComment!}", + dataIndex: "${x.propertyName!}", + }, + #} + #} + ]; + +export const searchFormSchema: FormSchema[] = [ + #for(x in prototypes) { + #if(x.isQuery==1){ + { + field: "${x.propertyName!}", + label: "${x.jdbcComment!}", + #if(x.componentType=="input"){ + component: 'Input', + #}else if(x.componentType=="textarea"){ + component: 'InputTextArea', + #}else if(x.componentType=="select"){ + component: 'ApiSelect', + #}else if(x.componentType=="tree"){ + component: 'ApiTreeSelect', + #}else if(x.componentType=="radio"){ + component: 'RadioGroup', + #}else if(x.componentType=="checkbox"){ + component: 'Checkbox', + #}else if(x.componentType=="switch"){ + component: 'Switch', + #}else if(x.componentType=="date"){ + component: 'DatePicker', + #} + #if(x.componentType=="select"&&x.dictCode!=null){ + componentProps: { + api: getDictList, + params: { code: '${x.dictCode!}' }, + labelField: 'dictValue', + valueField: 'dictKey', + }, + #} + }, + #} + #} +]; + +export const formSchema: FormSchema[] = [ + #for(x in prototypes) { + #if(x.isForm!=0){ + { + field: "${x.propertyName!}", + label: "${x.jdbcComment!}", + #if(x.componentType=="input"){ + component: 'Input', + #}else if(x.componentType=="textarea"){ + component: 'InputTextArea', + #}else if(x.componentType=="select"){ + component: 'ApiSelect', + #}else if(x.componentType=="tree"){ + component: 'ApiTreeSelect', + #}else if(x.componentType=="radio"){ + component: 'RadioGroup', + #}else if(x.componentType=="checkbox"){ + component: 'Checkbox', + #}else if(x.componentType=="switch"){ + component: 'Switch', + #}else if(x.componentType=="date"){ + component: 'DatePicker', + #} + #if(x.componentType=="select"&&x.dictCode!=null){ + componentProps: { + api: getDictList, + params: { code: '${x.dictCode!}' }, + labelField: 'dictValue', + valueField: 'dictKey', + }, + #} + #if(x.isRequired==1){ + required: true, + #} + }, + #} + #} +]; + +export const detailSchema: DescItem[] = [ + #for(x in prototypes) { + #if(x.isForm!=0){ + { + field: "${x.propertyName!}", + label: "${x.jdbcComment!}", + }, + #} + #} +]; diff --git a/blade-starter-develop/src/main/resources/templates/lemon/tree/data.ts.btl b/blade-starter-develop/src/main/resources/templates/lemon/tree/data.ts.btl new file mode 100644 index 0000000..9083b21 --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/lemon/tree/data.ts.btl @@ -0,0 +1,30 @@ +import { defHttp } from '@/utils/http/axios'; + +enum Api { + List = '/${serviceName!}/${modelCode!}/list', + Submit = '/${serviceName!}/${modelCode!}/submit', + Detail = '/${serviceName!}/${modelCode!}/detail', + Remove = '/${serviceName!}/${modelCode!}/remove', +} + +//列表 +export function getList(params?: object) { + return defHttp.get({ url: Api.List, params: params }, + { joinParamsToUrl: true, joinTime: false }); +} + +//提交 +export function submitObj(params?: object) { + return defHttp.post({ url: Api.Submit, params: params }); +} + +//详情 +export function getDetail(params?: object) { + return defHttp.get({ url: Api.Detail, params }); +} + +//删除 +export function remove(params?: object) { + return defHttp.post({ url: Api.Remove, params }, { joinParamsToUrl: true }); +} + diff --git a/blade-starter-develop/src/main/resources/templates/lemon/tree/index.vue.btl b/blade-starter-develop/src/main/resources/templates/lemon/tree/index.vue.btl new file mode 100644 index 0000000..3e03663 --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/lemon/tree/index.vue.btl @@ -0,0 +1,124 @@ + + diff --git a/blade-starter-develop/src/main/resources/templates/saber/crud/api.js.btl b/blade-starter-develop/src/main/resources/templates/saber/crud/api.js.btl new file mode 100644 index 0000000..fa12324 --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/saber/crud/api.js.btl @@ -0,0 +1,50 @@ +import request from '@/router/axios'; + +export const getList = (current, size, params) => { + return request({ + url: '/api/${serviceName!}/${modelCode!}/list', + method: 'get', + params: { + ...params, + current, + size, + } + }) +} + +export const getDetail = (id) => { + return request({ + url: '/api/${serviceName!}/${modelCode!}/detail', + method: 'get', + params: { + id + } + }) +} + +export const remove = (ids) => { + return request({ + url: '/api/${serviceName!}/${modelCode!}/remove', + method: 'post', + params: { + ids, + } + }) +} + +export const add = (row) => { + return request({ + url: '/api/${serviceName!}/${modelCode!}/submit', + method: 'post', + data: row + }) +} + +export const update = (row) => { + return request({ + url: '/api/${serviceName!}/${modelCode!}/submit', + method: 'post', + data: row + }) +} + diff --git a/blade-starter-develop/src/main/resources/templates/saber/crud/crud.vue.btl b/blade-starter-develop/src/main/resources/templates/saber/crud/crud.vue.btl new file mode 100644 index 0000000..02207bc --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/saber/crud/crud.vue.btl @@ -0,0 +1,247 @@ + + + + + diff --git a/blade-starter-develop/src/main/resources/templates/saber/crud/option.js.btl b/blade-starter-develop/src/main/resources/templates/saber/crud/option.js.btl new file mode 100644 index 0000000..1d21c9d --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/saber/crud/option.js.btl @@ -0,0 +1,67 @@ +export default { + height:'auto', + calcHeight: 30, + tip: false, + searchShow: true, + searchMenuSpan: 6, + border: true, + index: true, + viewBtn: true, + selection: true, + dialogClickModal: false, + column: [ +#for(x in prototypes) { + { + label: "${x.jdbcComment!}", + prop: "${x.propertyName!}", +#if(strutil.contain(x.componentType,"editor")){ + component: 'AvueUeditor', + options: { + action: '/api/blade-resource/oss/endpoint/put-file', + props: { + res: "data", + url: "link", + } + }, + hide: true, + minRows: 6, +#}else{ + type: "${x.componentType!}", +#} +#if(strutil.contain(x.componentType,"date")||strutil.contain(x.componentType,"time")){ + format: "yyyy-MM-dd hh:mm:ss", + valueFormat: "yyyy-MM-dd hh:mm:ss", +#} +#if(isNotEmpty(x.dictCode)){ + dicUrl: "/api/blade-system/dict/dictionary?code=${x.dictCode!}", + dataType: "number", + props: { + label: "dictValue", + value: "dictKey" + }, +#} +#if(x.isForm==0){ + addDisplay: false, + editDisplay: false, + viewDisplay: false, +#} +#if(x.isRow==1){ + span: 24, +#} +#if(x.isList==0){ + hide: true, +#} +#if(x.isQuery==1){ + search: true, +#} +#if(x.isRequired==1){ + rules: [{ + required: true, + message: "请输入${x.jdbcComment!}", + trigger: "blur" + }], +#} + }, +#} + ] +} diff --git a/blade-starter-develop/src/main/resources/templates/saber/sub/api.js.btl b/blade-starter-develop/src/main/resources/templates/saber/sub/api.js.btl new file mode 100644 index 0000000..fa12324 --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/saber/sub/api.js.btl @@ -0,0 +1,50 @@ +import request from '@/router/axios'; + +export const getList = (current, size, params) => { + return request({ + url: '/api/${serviceName!}/${modelCode!}/list', + method: 'get', + params: { + ...params, + current, + size, + } + }) +} + +export const getDetail = (id) => { + return request({ + url: '/api/${serviceName!}/${modelCode!}/detail', + method: 'get', + params: { + id + } + }) +} + +export const remove = (ids) => { + return request({ + url: '/api/${serviceName!}/${modelCode!}/remove', + method: 'post', + params: { + ids, + } + }) +} + +export const add = (row) => { + return request({ + url: '/api/${serviceName!}/${modelCode!}/submit', + method: 'post', + data: row + }) +} + +export const update = (row) => { + return request({ + url: '/api/${serviceName!}/${modelCode!}/submit', + method: 'post', + data: row + }) +} + diff --git a/blade-starter-develop/src/main/resources/templates/saber/sub/crud.vue.btl b/blade-starter-develop/src/main/resources/templates/saber/sub/crud.vue.btl new file mode 100644 index 0000000..54f64d3 --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/saber/sub/crud.vue.btl @@ -0,0 +1,446 @@ + + + + + diff --git a/blade-starter-develop/src/main/resources/templates/saber/sub/option.js.btl b/blade-starter-develop/src/main/resources/templates/saber/sub/option.js.btl new file mode 100644 index 0000000..766dac3 --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/saber/sub/option.js.btl @@ -0,0 +1,67 @@ +export default { + height:'auto', + calcHeight: 30, + tip: false, + searchShow: true, + searchMenuSpan: 6, + border: true, + index: true, + viewBtn: true, + selection: true, + menuWidth: 300, + column: [ +#for(x in prototypes) { + { + label: "${x.jdbcComment!}", + prop: "${x.propertyName!}", +#if(strutil.contain(x.componentType,"editor")){ + component: 'AvueUeditor', + options: { + action: '/api/blade-resource/oss/endpoint/put-file', + props: { + res: "data", + url: "link", + } + }, + hide: true, + minRows: 6, +#}else{ + type: "${x.componentType!}", +#} +#if(strutil.contain(x.componentType,"date")||strutil.contain(x.componentType,"time")){ + format: "yyyy-MM-dd hh:mm:ss", + valueFormat: "yyyy-MM-dd hh:mm:ss", +#} +#if(isNotEmpty(x.dictCode)){ + dicUrl: "/api/blade-system/dict/dictionary?code=${x.dictCode!}", + dataType: "number", + props: { + label: "dictValue", + value: "dictKey" + }, +#} +#if(x.isForm==0){ + addDisplay: false, + editDisplay: false, + viewDisplay: false, +#} +#if(x.isRow==1){ + span: 24, +#} +#if(x.isList==0){ + hide: true, +#} +#if(x.isQuery==1){ + search: true, +#} +#if(x.isRequired==1&&isEmpty(x.validateRule)){ + rules: [{ + required: true, + message: "请输入${x.jdbcComment!}", + trigger: "blur" + }], +#} + }, +#} + ] +} diff --git a/blade-starter-develop/src/main/resources/templates/saber/tree/api.js.btl b/blade-starter-develop/src/main/resources/templates/saber/tree/api.js.btl new file mode 100644 index 0000000..ff4186a --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/saber/tree/api.js.btl @@ -0,0 +1,60 @@ +import request from '@/router/axios'; + +export const getList = (current, size, params) => { + return request({ + url: '/api/${serviceName!}/${modelCode!}/list', + method: 'get', + params: { + ...params, + current, + size, + } + }) +} + +export const getDetail = (id) => { + return request({ + url: '/api/${serviceName!}/${modelCode!}/detail', + method: 'get', + params: { + id + } + }) +} + +export const getTree = (tenantId) => { + return request({ + url: '/api/${serviceName!}/${modelCode!}/tree', + method: 'get', + params: { + tenantId, + } + }) +} + +export const remove = (ids) => { + return request({ + url: '/api/${serviceName!}/${modelCode!}/remove', + method: 'post', + params: { + ids, + } + }) +} + +export const add = (row) => { + return request({ + url: '/api/${serviceName!}/${modelCode!}/submit', + method: 'post', + data: row + }) +} + +export const update = (row) => { + return request({ + url: '/api/${serviceName!}/${modelCode!}/submit', + method: 'post', + data: row + }) +} + diff --git a/blade-starter-develop/src/main/resources/templates/saber/tree/crud.vue.btl b/blade-starter-develop/src/main/resources/templates/saber/tree/crud.vue.btl new file mode 100644 index 0000000..45de1ca --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/saber/tree/crud.vue.btl @@ -0,0 +1,248 @@ + + + + + diff --git a/blade-starter-develop/src/main/resources/templates/saber/tree/option.js.btl b/blade-starter-develop/src/main/resources/templates/saber/tree/option.js.btl new file mode 100644 index 0000000..4673615 --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/saber/tree/option.js.btl @@ -0,0 +1,74 @@ +export default { + height:'auto', + calcHeight: 30, + tip: false, + searchShow: true, + searchMenuSpan: 6, + border: true, + index: true, + viewBtn: true, + selection: true, + dialogClickModal: false, + column: [ +#for(x in prototypes) { + { + label: "${x.jdbcComment!}", + prop: "${x.propertyName!}", + #if(x.propertyName==treePidHump){ + type: "tree", + dicData: [], + props: { + label: "title", + }, + value: 0, + #}else if(strutil.contain(x.componentType,"editor")){ + component: 'AvueUeditor', + options: { + action: '/api/blade-resource/oss/endpoint/put-file', + props: { + res: "data", + url: "link", + } + }, + hide: true, + minRows: 6, + #}else{ + type: "${x.componentType!}", + #} +#if(strutil.contain(x.componentType,"date")||strutil.contain(x.componentType,"time")){ + format: "yyyy-MM-dd hh:mm:ss", + valueFormat: "yyyy-MM-dd hh:mm:ss", +#} +#if(isNotEmpty(x.dictCode)&&x.propertyName!=treePidHump){ + dicUrl: "/api/blade-system/dict/dictionary?code=${x.dictCode!}", + dataType: "number", + props: { + label: "dictValue", + value: "dictKey" + }, +#} +#if(x.isForm==0){ + addDisplay: false, + editDisplay: false, + viewDisplay: false, +#} +#if(x.isRow==1){ + span: 24, +#} +#if(x.isList==0){ + hide: true, +#} +#if(x.isQuery==1){ + search: true, +#} +#if(x.isRequired==1){ + rules: [{ + required: true, + message: "请输入${x.jdbcComment!}", + trigger: "blur" + }], +#} + }, +#} + ] +} diff --git a/blade-starter-develop/src/main/resources/templates/saber3/crud/api.js.btl b/blade-starter-develop/src/main/resources/templates/saber3/crud/api.js.btl new file mode 100644 index 0000000..db6dabe --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/saber3/crud/api.js.btl @@ -0,0 +1,50 @@ +import request from '@/axios'; + +export const getList = (current, size, params) => { + return request({ + url: '/${serviceName!}/${modelCode!}/list', + method: 'get', + params: { + ...params, + current, + size, + } + }) +} + +export const getDetail = (id) => { + return request({ + url: '/${serviceName!}/${modelCode!}/detail', + method: 'get', + params: { + id + } + }) +} + +export const remove = (ids) => { + return request({ + url: '/${serviceName!}/${modelCode!}/remove', + method: 'post', + params: { + ids, + } + }) +} + +export const add = (row) => { + return request({ + url: '/${serviceName!}/${modelCode!}/submit', + method: 'post', + data: row + }) +} + +export const update = (row) => { + return request({ + url: '/${serviceName!}/${modelCode!}/submit', + method: 'post', + data: row + }) +} + diff --git a/blade-starter-develop/src/main/resources/templates/saber3/crud/crud.vue.btl b/blade-starter-develop/src/main/resources/templates/saber3/crud/crud.vue.btl new file mode 100644 index 0000000..271a42b --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/saber3/crud/crud.vue.btl @@ -0,0 +1,245 @@ + + + + + diff --git a/blade-starter-develop/src/main/resources/templates/saber3/crud/option.js.btl b/blade-starter-develop/src/main/resources/templates/saber3/crud/option.js.btl new file mode 100644 index 0000000..4f4e68a --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/saber3/crud/option.js.btl @@ -0,0 +1,67 @@ +export default { + height:'auto', + calcHeight: 30, + tip: false, + searchShow: true, + searchMenuSpan: 6, + border: true, + index: true, + viewBtn: true, + selection: true, + dialogClickModal: false, + column: [ +#for(x in prototypes) { + { + label: "${x.jdbcComment!}", + prop: "${x.propertyName!}", +#if(strutil.contain(x.componentType,"editor")){ + component: 'avue-ueditor', + options: { + action: '/blade-resource/oss/endpoint/put-file', + props: { + res: "data", + url: "link", + } + }, + hide: true, + minRows: 6, +#}else{ + type: "${x.componentType!}", +#} +#if(strutil.contain(x.componentType,"date")||strutil.contain(x.componentType,"time")){ + format: "YYYY-MM-DD HH:mm:ss", + valueFormat: "YYYY-MM-DD HH:mm:ss", +#} +#if(isNotEmpty(x.dictCode)){ + dicUrl: "/blade-system/dict/dictionary?code=${x.dictCode!}", + dataType: "number", + props: { + label: "dictValue", + value: "dictKey" + }, +#} +#if(x.isForm==0){ + addDisplay: false, + editDisplay: false, + viewDisplay: false, +#} +#if(x.isRow==1){ + span: 24, +#} +#if(x.isList==0){ + hide: true, +#} +#if(x.isQuery==1){ + search: true, +#} +#if(x.isRequired==1){ + rules: [{ + required: true, + message: "请输入${x.jdbcComment!}", + trigger: "blur" + }], +#} + }, +#} + ] +} diff --git a/blade-starter-develop/src/main/resources/templates/saber3/sub/api.js.btl b/blade-starter-develop/src/main/resources/templates/saber3/sub/api.js.btl new file mode 100644 index 0000000..db6dabe --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/saber3/sub/api.js.btl @@ -0,0 +1,50 @@ +import request from '@/axios'; + +export const getList = (current, size, params) => { + return request({ + url: '/${serviceName!}/${modelCode!}/list', + method: 'get', + params: { + ...params, + current, + size, + } + }) +} + +export const getDetail = (id) => { + return request({ + url: '/${serviceName!}/${modelCode!}/detail', + method: 'get', + params: { + id + } + }) +} + +export const remove = (ids) => { + return request({ + url: '/${serviceName!}/${modelCode!}/remove', + method: 'post', + params: { + ids, + } + }) +} + +export const add = (row) => { + return request({ + url: '/${serviceName!}/${modelCode!}/submit', + method: 'post', + data: row + }) +} + +export const update = (row) => { + return request({ + url: '/${serviceName!}/${modelCode!}/submit', + method: 'post', + data: row + }) +} + diff --git a/blade-starter-develop/src/main/resources/templates/saber3/sub/crud.vue.btl b/blade-starter-develop/src/main/resources/templates/saber3/sub/crud.vue.btl new file mode 100644 index 0000000..283ce03 --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/saber3/sub/crud.vue.btl @@ -0,0 +1,442 @@ + + + + + diff --git a/blade-starter-develop/src/main/resources/templates/saber3/sub/option.js.btl b/blade-starter-develop/src/main/resources/templates/saber3/sub/option.js.btl new file mode 100644 index 0000000..cd17794 --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/saber3/sub/option.js.btl @@ -0,0 +1,67 @@ +export default { + height:'auto', + calcHeight: 30, + tip: false, + searchShow: true, + searchMenuSpan: 6, + border: true, + index: true, + viewBtn: true, + selection: true, + menuWidth: 300, + column: [ +#for(x in prototypes) { + { + label: "${x.jdbcComment!}", + prop: "${x.propertyName!}", +#if(strutil.contain(x.componentType,"editor")){ + component: 'avue-ueditor', + options: { + action: '/blade-resource/oss/endpoint/put-file', + props: { + res: "data", + url: "link", + } + }, + hide: true, + minRows: 6, +#}else{ + type: "${x.componentType!}", +#} +#if(strutil.contain(x.componentType,"date")||strutil.contain(x.componentType,"time")){ + format: "YYYY-MM-DD HH:mm:ss", + valueFormat: "YYYY-MM-DD HH:mm:ss", +#} +#if(isNotEmpty(x.dictCode)){ + dicUrl: "/blade-system/dict/dictionary?code=${x.dictCode!}", + dataType: "number", + props: { + label: "dictValue", + value: "dictKey" + }, +#} +#if(x.isForm==0){ + addDisplay: false, + editDisplay: false, + viewDisplay: false, +#} +#if(x.isRow==1){ + span: 24, +#} +#if(x.isList==0){ + hide: true, +#} +#if(x.isQuery==1){ + search: true, +#} +#if(x.isRequired==1&&isEmpty(x.validateRule)){ + rules: [{ + required: true, + message: "请输入${x.jdbcComment!}", + trigger: "blur" + }], +#} + }, +#} + ] +} diff --git a/blade-starter-develop/src/main/resources/templates/saber3/tree/api.js.btl b/blade-starter-develop/src/main/resources/templates/saber3/tree/api.js.btl new file mode 100644 index 0000000..1965fb5 --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/saber3/tree/api.js.btl @@ -0,0 +1,60 @@ +import request from '@/axios'; + +export const getList = (current, size, params) => { + return request({ + url: '/${serviceName!}/${modelCode!}/list', + method: 'get', + params: { + ...params, + current, + size, + } + }) +} + +export const getDetail = (id) => { + return request({ + url: '/${serviceName!}/${modelCode!}/detail', + method: 'get', + params: { + id + } + }) +} + +export const getTree = (tenantId) => { + return request({ + url: '/${serviceName!}/${modelCode!}/tree', + method: 'get', + params: { + tenantId, + } + }) +} + +export const remove = (ids) => { + return request({ + url: '/${serviceName!}/${modelCode!}/remove', + method: 'post', + params: { + ids, + } + }) +} + +export const add = (row) => { + return request({ + url: '/${serviceName!}/${modelCode!}/submit', + method: 'post', + data: row + }) +} + +export const update = (row) => { + return request({ + url: '/${serviceName!}/${modelCode!}/submit', + method: 'post', + data: row + }) +} + diff --git a/blade-starter-develop/src/main/resources/templates/saber3/tree/crud.vue.btl b/blade-starter-develop/src/main/resources/templates/saber3/tree/crud.vue.btl new file mode 100644 index 0000000..8841640 --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/saber3/tree/crud.vue.btl @@ -0,0 +1,246 @@ + + + + + diff --git a/blade-starter-develop/src/main/resources/templates/saber3/tree/option.js.btl b/blade-starter-develop/src/main/resources/templates/saber3/tree/option.js.btl new file mode 100644 index 0000000..aad7327 --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/saber3/tree/option.js.btl @@ -0,0 +1,74 @@ +export default { + height:'auto', + calcHeight: 30, + tip: false, + searchShow: true, + searchMenuSpan: 6, + border: true, + index: true, + viewBtn: true, + selection: true, + dialogClickModal: false, + column: [ +#for(x in prototypes) { + { + label: "${x.jdbcComment!}", + prop: "${x.propertyName!}", + #if(x.propertyName==treePidHump){ + type: "tree", + dicData: [], + props: { + label: "title", + }, + value: 0, + #}else if(strutil.contain(x.componentType,"editor")){ + component: 'avue-ueditor', + options: { + action: '/blade-resource/oss/endpoint/put-file', + props: { + res: "data", + url: "link", + } + }, + hide: true, + minRows: 6, + #}else{ + type: "${x.componentType!}", + #} +#if(strutil.contain(x.componentType,"date")||strutil.contain(x.componentType,"time")){ + format: "YYYY-MM-DD HH:mm:ss", + valueFormat: "YYYY-MM-DD HH:mm:ss", +#} +#if(isNotEmpty(x.dictCode)&&x.propertyName!=treePidHump){ + dicUrl: "/blade-system/dict/dictionary?code=${x.dictCode!}", + dataType: "number", + props: { + label: "dictValue", + value: "dictKey" + }, +#} +#if(x.isForm==0){ + addDisplay: false, + editDisplay: false, + viewDisplay: false, +#} +#if(x.isRow==1){ + span: 24, +#} +#if(x.isList==0){ + hide: true, +#} +#if(x.isQuery==1){ + search: true, +#} +#if(x.isRequired==1){ + rules: [{ + required: true, + message: "请输入${x.jdbcComment!}", + trigger: "blur" + }], +#} + }, +#} + ] +} diff --git a/blade-starter-develop/src/main/resources/templates/sql/menu.sql.btl b/blade-starter-develop/src/main/resources/templates/sql/menu.sql.btl new file mode 100644 index 0000000..246f680 --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/sql/menu.sql.btl @@ -0,0 +1,10 @@ +INSERT INTO `blade_menu`(`id`, `parent_id`, `code`, `name`, `alias`, `path`, `source`, `sort`, `category`, `action`, `is_open`, `remark`, `is_deleted`) +VALUES ('${menuId}', 1123598815738675201, '${modelCode!}', '${codeName!}', 'menu', '/${serviceCode!}/${modelCode!}', NULL, 1, 1, 0, 1, NULL, 0); +INSERT INTO `blade_menu`(`id`, `parent_id`, `code`, `name`, `alias`, `path`, `source`, `sort`, `category`, `action`, `is_open`, `remark`, `is_deleted`) +VALUES ('${addMenuId}', '${menuId}', '${modelCode!}_add', '新增', 'add', '/${serviceCode!}/${modelCode!}/add', 'plus', 1, 2, 1, 1, NULL, 0); +INSERT INTO `blade_menu`(`id`, `parent_id`, `code`, `name`, `alias`, `path`, `source`, `sort`, `category`, `action`, `is_open`, `remark`, `is_deleted`) +VALUES ('${editMenuId}', '${menuId}', '${modelCode!}_edit', '修改', 'edit', '/${serviceCode!}/${modelCode!}/edit', 'form', 2, 2, 2, 1, NULL, 0); +INSERT INTO `blade_menu`(`id`, `parent_id`, `code`, `name`, `alias`, `path`, `source`, `sort`, `category`, `action`, `is_open`, `remark`, `is_deleted`) +VALUES ('${removeMenuId}', '${menuId}', '${modelCode!}_delete', '删除', 'delete', '/api/${serviceName!}/${modelCode!}/remove', 'delete', 3, 2, 3, 1, NULL, 0); +INSERT INTO `blade_menu`(`id`, `parent_id`, `code`, `name`, `alias`, `path`, `source`, `sort`, `category`, `action`, `is_open`, `remark`, `is_deleted`) +VALUES ('${viewMenuId}', '${menuId}', '${modelCode!}_view', '查看', 'view', '/${serviceCode!}/${modelCode!}/view', 'file-text', 4, 2, 2, 1, NULL, 0); diff --git a/blade-starter-develop/src/main/resources/templates/sword/action.js.vm b/blade-starter-develop/src/main/resources/templates/sword/action.js.vm new file mode 100644 index 0000000..e0eb476 --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/sword/action.js.vm @@ -0,0 +1,37 @@ +#set($upperEntityPath=$table.entityPath.toUpperCase()) +export const $!{upperEntityPath}_NAMESPACE = '$!{table.entityPath}'; + +export function $!{upperEntityPath}_LIST(payload) { + return { + type: `${$!{upperEntityPath}_NAMESPACE}/fetchList`, + payload, + }; +} + +export function $!{upperEntityPath}_DETAIL(id) { + return { + type: `${$!{upperEntityPath}_NAMESPACE}/fetchDetail`, + payload: { id }, + }; +} + +export function $!{upperEntityPath}_CLEAR_DETAIL() { + return { + type: `${$!{upperEntityPath}_NAMESPACE}/clearDetail`, + payload: {}, + }; +} + +export function $!{upperEntityPath}_SUBMIT(payload) { + return { + type: `${$!{upperEntityPath}_NAMESPACE}/submit`, + payload, + }; +} + +export function $!{upperEntityPath}_REMOVE(payload) { + return { + type: `${$!{upperEntityPath}_NAMESPACE}/remove`, + payload, + }; +} diff --git a/blade-starter-develop/src/main/resources/templates/sword/add.js.vm b/blade-starter-develop/src/main/resources/templates/sword/add.js.vm new file mode 100644 index 0000000..dd17383 --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/sword/add.js.vm @@ -0,0 +1,75 @@ +#set($upperEntityPath=$table.entityPath.toUpperCase()) +import React, { PureComponent } from 'react'; +import { Form, Input, Card, Button } from 'antd'; +import { connect } from 'dva'; +import Panel from '../../../components/Panel'; +import styles from '../../../layouts/Sword.less'; +import { $!{upperEntityPath}_SUBMIT } from '../../../actions/$!{table.entityPath}'; + +const FormItem = Form.Item; + +@connect(({ loading }) => ({ + submitting: loading.effects['$!{table.entityPath}/submit'], +})) +@Form.create() +class $!{entity}Add extends PureComponent { + handleSubmit = e => { + e.preventDefault(); + const { dispatch, form } = this.props; + form.validateFieldsAndScroll((err, values) => { + if (!err) { + dispatch($!{upperEntityPath}_SUBMIT(values)); + } + }); + }; + + render() { + const { + form: { getFieldDecorator }, + submitting, + } = this.props; + + const formItemLayout = { + labelCol: { + xs: { span: 24 }, + sm: { span: 7 }, + }, + wrapperCol: { + xs: { span: 24 }, + sm: { span: 12 }, + md: { span: 10 }, + }, + }; + + const action = ( + + ); + + return ( + +

+ +#foreach($field in $!{table.fields}) +#if($!{field.name}!=$!{tenantColumn}) + + {getFieldDecorator('$!{field.propertyName}', { + rules: [ + { + required: true, + message: '请输入$!{field.comment}', + }, + ], + })()} + +#end +#end + +
+ + ); + } +} + +export default $!{entity}Add; diff --git a/blade-starter-develop/src/main/resources/templates/sword/edit.js.vm b/blade-starter-develop/src/main/resources/templates/sword/edit.js.vm new file mode 100644 index 0000000..7a4f33d --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/sword/edit.js.vm @@ -0,0 +1,99 @@ +#set($upperEntityPath=$table.entityPath.toUpperCase()) +import React, { PureComponent } from 'react'; +import { Form, Input, Card, Button } from 'antd'; +import { connect } from 'dva'; +import Panel from '../../../components/Panel'; +import styles from '../../../layouts/Sword.less'; +import { $!{upperEntityPath}_DETAIL, $!{upperEntityPath}_SUBMIT } from '../../../actions/$!{table.entityPath}'; + +const FormItem = Form.Item; + +@connect(({ $!{table.entityPath}, loading }) => ({ + $!{table.entityPath}, + submitting: loading.effects['$!{table.entityPath}/submit'], +})) +@Form.create() +class $!{entity}Edit extends PureComponent { + componentWillMount() { + const { + dispatch, + match: { + params: { id }, + }, + } = this.props; + dispatch($!{upperEntityPath}_DETAIL(id)); + } + + handleSubmit = e => { + e.preventDefault(); + const { + dispatch, + match: { + params: { id }, + }, + form, + } = this.props; + form.validateFieldsAndScroll((err, values) => { + if (!err) { + const params = { + id, + ...values, + }; + console.log(params); + dispatch($!{upperEntityPath}_SUBMIT(params)); + } + }); + }; + + render() { + const { + form: { getFieldDecorator }, + $!{table.entityPath}: { detail }, + submitting, + } = this.props; + + const formItemLayout = { + labelCol: { + xs: { span: 24 }, + sm: { span: 7 }, + }, + wrapperCol: { + xs: { span: 24 }, + sm: { span: 12 }, + md: { span: 10 }, + }, + }; + + const action = ( + + ); + + return ( + +
+ +#foreach($field in $!{table.fields}) +#if($!{field.name}!=$!{tenantColumn}) + + {getFieldDecorator('$!{field.propertyName}', { + rules: [ + { + required: true, + message: '请输入$!{field.comment}', + }, + ], + initialValue: detail.$!{field.propertyName}, + })()} + +#end +#end + +
+
+ ); + } +} + +export default $!{entity}Edit; diff --git a/blade-starter-develop/src/main/resources/templates/sword/list.js.vm b/blade-starter-develop/src/main/resources/templates/sword/list.js.vm new file mode 100644 index 0000000..298de7e --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/sword/list.js.vm @@ -0,0 +1,84 @@ +#set($upperEntityPath=$table.entityPath.toUpperCase()) +import React, { PureComponent } from 'react'; +import { connect } from 'dva'; +import { Button, Col, Form, Input, Row } from 'antd'; +import Panel from '../../../components/Panel'; +import { $!{upperEntityPath}_LIST } from '../../../actions/$!{table.entityPath}'; +import Grid from '../../../components/Sword/Grid'; + +const FormItem = Form.Item; + +@connect(({ $!{table.entityPath}, loading }) => ({ + $!{table.entityPath}, + loading: loading.models.$!{table.entityPath}, +})) +@Form.create() +class $!{entity} extends PureComponent { + // ============ 查询 =============== + handleSearch = params => { + const { dispatch } = this.props; + dispatch($!{upperEntityPath}_LIST(params)); + }; + + // ============ 查询表单 =============== + renderSearchForm = onReset => { + const { form } = this.props; + const { getFieldDecorator } = form; + + return ( + + + + {getFieldDecorator('name')()} + + + +
+ + +
+ +
+ ); + }; + + render() { + const code = '$!{table.entityPath}'; + + const { + form, + loading, + $!{table.entityPath}: { data }, + } = this.props; + + const columns = [ +#foreach($field in $!{table.fields}) +#if($!{field.name}!=$!{tenantColumn}) + { + title: '$!{field.comment}', + dataIndex: '$!{field.propertyName}', + }, +#end +#end + ]; + + return ( + + + + ); + } +} +export default $!{entity}; diff --git a/blade-starter-develop/src/main/resources/templates/sword/model.js.vm b/blade-starter-develop/src/main/resources/templates/sword/model.js.vm new file mode 100644 index 0000000..6188988 --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/sword/model.js.vm @@ -0,0 +1,88 @@ +#set($upperEntityPath=$table.entityPath.toUpperCase()) +import { message } from 'antd'; +import router from 'umi/router'; +import { $!{upperEntityPath}_NAMESPACE } from '../actions/$!{table.entityPath}'; +import { list, submit, detail, remove } from '../services/$!{table.entityPath}'; + +export default { + namespace: $!{upperEntityPath}_NAMESPACE, + state: { + data: { + list: [], + pagination: false, + }, + detail: {}, + }, + effects: { + *fetchList({ payload }, { call, put }) { + const response = yield call(list, payload); + if (response.success) { + yield put({ + type: 'saveList', + payload: { + list: response.data.records, + pagination: { + total: response.data.total, + current: response.data.current, + pageSize: response.data.size, + }, + }, + }); + } + }, + *fetchDetail({ payload }, { call, put }) { + const response = yield call(detail, payload); + if (response.success) { + yield put({ + type: 'saveDetail', + payload: { + detail: response.data, + }, + }); + } + }, + *clearDetail({ payload }, { put }) { + yield put({ + type: 'removeDetail', + payload: { payload }, + }); + }, + *submit({ payload }, { call }) { + const response = yield call(submit, payload); + if (response.success) { + message.success('提交成功'); + router.push('/$!{servicePackage}/$!{table.entityPath}'); + } + }, + *remove({ payload }, { call }) { + const { + data: { keys }, + success, + } = payload; + const response = yield call(remove, { ids: keys }); + if (response.success) { + success(); + } + }, + }, + reducers: { + saveList(state, action) { + return { + ...state, + data: action.payload, + }; + }, + saveDetail(state, action) { + return { + ...state, + detail: action.payload.detail, + }; + }, + removeDetail(state) { + return { + ...state, + detail: {}, + }; + }, + }, +}; diff --git a/blade-starter-develop/src/main/resources/templates/sword/service.js.vm b/blade-starter-develop/src/main/resources/templates/sword/service.js.vm new file mode 100644 index 0000000..cf3d16e --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/sword/service.js.vm @@ -0,0 +1,26 @@ +#set($params="$" + "{stringify" + "(params)" + "}") +import { stringify } from 'qs'; +import func from '../utils/Func'; +import request from '../utils/request'; + +export async function list(params) { + return request(`/api/$!{serviceName}/$!{entityKey}/list?$!{params}`); +} + +export async function submit(params) { + return request('/api/$!{serviceName}/$!{entityKey}/submit', { + method: 'POST', + body: params, + }); +} + +export async function detail(params) { + return request(`/api/$!{serviceName}/$!{entityKey}/detail?$!{params}`); +} + +export async function remove(params) { + return request('/api/$!{serviceName}/$!{entityKey}/remove', { + method: 'POST', + body: func.toFormData(params), + }); +} diff --git a/blade-starter-develop/src/main/resources/templates/sword/view.js.vm b/blade-starter-develop/src/main/resources/templates/sword/view.js.vm new file mode 100644 index 0000000..397b23e --- /dev/null +++ b/blade-starter-develop/src/main/resources/templates/sword/view.js.vm @@ -0,0 +1,77 @@ +#set($upperEntityPath=$table.entityPath.toUpperCase()) +#set($editId="$" + "{" + "id" + "}") +import React, { PureComponent } from 'react'; +import router from 'umi/router'; +import { Form, Card, Button } from 'antd'; +import { connect } from 'dva'; +import Panel from '../../../components/Panel'; +import styles from '../../../layouts/Sword.less'; +import { $!{upperEntityPath}_DETAIL } from '../../../actions/$!{table.entityPath}'; + +const FormItem = Form.Item; + +@connect(({ $!{table.entityPath} }) => ({ + $!{table.entityPath}, +})) +@Form.create() +class $!{entity}View extends PureComponent { + componentWillMount() { + const { + dispatch, + match: { + params: { id }, + }, + } = this.props; + dispatch($!{upperEntityPath}_DETAIL(id)); + } + + handleEdit = () => { + const { + match: { + params: { id }, + }, + } = this.props; + router.push(`/$!{servicePackage}/$!{table.entityPath}/edit/$!{editId}`); + }; + + render() { + const { + $!{table.entityPath}: { detail }, + } = this.props; + + const formItemLayout = { + labelCol: { + xs: { span: 24 }, + sm: { span: 7 }, + }, + wrapperCol: { + xs: { span: 24 }, + sm: { span: 12 }, + md: { span: 10 }, + }, + }; + + const action = ( + + ); + + return ( + +
+ +#foreach($field in $!{table.fields}) +#if($!{field.name}!=$!{tenantColumn}) + + {detail.$!{field.propertyName}} + +#end +#end + +
+
+ ); + } +} +export default $!{entity}View; diff --git a/blade-starter-ehcache/pom.xml b/blade-starter-ehcache/pom.xml new file mode 100644 index 0000000..0a85b34 --- /dev/null +++ b/blade-starter-ehcache/pom.xml @@ -0,0 +1,29 @@ + + + 4.0.0 + + BladeX-Tool + org.springblade + ${revision} + + + blade-starter-ehcache + ${project.artifactId} + ${project.parent.version} + jar + + + + org.springframework.boot + spring-boot-starter-cache + + + net.sf.ehcache + ehcache + 2.10.5 + + + + diff --git a/blade-starter-ehcache/src/main/java/org/springblade/core/ehcache/EhcacheConfiguration.java b/blade-starter-ehcache/src/main/java/org/springblade/core/ehcache/EhcacheConfiguration.java new file mode 100644 index 0000000..cecb269 --- /dev/null +++ b/blade-starter-ehcache/src/main/java/org/springblade/core/ehcache/EhcacheConfiguration.java @@ -0,0 +1,39 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.ehcache; + +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.cache.annotation.EnableCaching; + +/** + * Ehcache配置类 + * + * @author Chill + */ +@EnableCaching +@AutoConfiguration +public class EhcacheConfiguration { +} diff --git a/blade-starter-ehcache/src/main/resources/ehcache.xml b/blade-starter-ehcache/src/main/resources/ehcache.xml new file mode 100644 index 0000000..1c6ad0c --- /dev/null +++ b/blade-starter-ehcache/src/main/resources/ehcache.xml @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/blade-starter-excel/pom.xml b/blade-starter-excel/pom.xml new file mode 100644 index 0000000..9e8a8b7 --- /dev/null +++ b/blade-starter-excel/pom.xml @@ -0,0 +1,29 @@ + + + 4.0.0 + + BladeX-Tool + org.springblade + ${revision} + + + blade-starter-excel + ${project.artifactId} + ${project.parent.version} + jar + + + + org.springblade + blade-core-launch + + + com.alibaba + easyexcel + + + + + diff --git a/blade-starter-excel/src/main/java/org/springblade/core/excel/listener/DataListener.java b/blade-starter-excel/src/main/java/org/springblade/core/excel/listener/DataListener.java new file mode 100644 index 0000000..8bf7b7f --- /dev/null +++ b/blade-starter-excel/src/main/java/org/springblade/core/excel/listener/DataListener.java @@ -0,0 +1,60 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.excel.listener; + +import com.alibaba.excel.context.AnalysisContext; +import com.alibaba.excel.event.AnalysisEventListener; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +import java.util.ArrayList; +import java.util.List; + +/** + * Excel监听器 + * + * @author Chill + */ +@Getter +@EqualsAndHashCode(callSuper = true) +public class DataListener extends AnalysisEventListener { + + /** + * 缓存的数据列表 + */ + private final List dataList = new ArrayList<>(); + + @Override + public void invoke(T data, AnalysisContext analysisContext) { + dataList.add(data); + } + + @Override + public void doAfterAllAnalysed(AnalysisContext analysisContext) { + + } + +} diff --git a/blade-starter-excel/src/main/java/org/springblade/core/excel/listener/ImportListener.java b/blade-starter-excel/src/main/java/org/springblade/core/excel/listener/ImportListener.java new file mode 100644 index 0000000..a0ee232 --- /dev/null +++ b/blade-starter-excel/src/main/java/org/springblade/core/excel/listener/ImportListener.java @@ -0,0 +1,81 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.excel.listener; + +import com.alibaba.excel.context.AnalysisContext; +import com.alibaba.excel.event.AnalysisEventListener; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.RequiredArgsConstructor; +import org.springblade.core.excel.support.ExcelImporter; + +import java.util.ArrayList; +import java.util.List; + +/** + * Excel监听器 + * + * @author Chill + */ +@Data +@RequiredArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class ImportListener extends AnalysisEventListener { + + /** + * 默认每隔3000条存储数据库 + */ + private int batchCount = 3000; + /** + * 缓存的数据列表 + */ + private List list = new ArrayList<>(); + /** + * 数据导入类 + */ + private final ExcelImporter importer; + + @Override + public void invoke(T data, AnalysisContext analysisContext) { + list.add(data); + // 达到BATCH_COUNT,则调用importer方法入库,防止数据几万条数据在内存,容易OOM + if (list.size() >= batchCount) { + // 调用importer方法 + importer.save(list); + // 存储完成清理list + list.clear(); + } + } + + @Override + public void doAfterAllAnalysed(AnalysisContext analysisContext) { + // 调用importer方法 + importer.save(list); + // 存储完成清理list + list.clear(); + } + +} diff --git a/blade-starter-excel/src/main/java/org/springblade/core/excel/support/ExcelException.java b/blade-starter-excel/src/main/java/org/springblade/core/excel/support/ExcelException.java new file mode 100644 index 0000000..b6f1d1b --- /dev/null +++ b/blade-starter-excel/src/main/java/org/springblade/core/excel/support/ExcelException.java @@ -0,0 +1,42 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.excel.support; + +import java.io.Serial; + +/** + * Excel异常处理类 + * + * @author Chill + */ +public class ExcelException extends RuntimeException { + @Serial + private static final long serialVersionUID = 1L; + + public ExcelException(String message) { + super(message); + } +} diff --git a/blade-starter-excel/src/main/java/org/springblade/core/excel/support/ExcelImporter.java b/blade-starter-excel/src/main/java/org/springblade/core/excel/support/ExcelImporter.java new file mode 100644 index 0000000..39a7962 --- /dev/null +++ b/blade-starter-excel/src/main/java/org/springblade/core/excel/support/ExcelImporter.java @@ -0,0 +1,44 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.excel.support; + +import java.util.List; + +/** + * Excel统一导入接口 + * + * @author Chill + */ +public interface ExcelImporter { + + /** + * 导入数据逻辑 + * + * @param data 数据集合 + */ + void save(List data); + +} diff --git a/blade-starter-excel/src/main/java/org/springblade/core/excel/util/ExcelUtil.java b/blade-starter-excel/src/main/java/org/springblade/core/excel/util/ExcelUtil.java new file mode 100644 index 0000000..59adb36 --- /dev/null +++ b/blade-starter-excel/src/main/java/org/springblade/core/excel/util/ExcelUtil.java @@ -0,0 +1,197 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.excel.util; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.read.builder.ExcelReaderBuilder; +import com.alibaba.excel.read.listener.ReadListener; +import com.alibaba.excel.util.DateUtils; +import com.alibaba.excel.write.builder.ExcelWriterBuilder; +import com.alibaba.excel.write.handler.WriteHandler; +import jakarta.servlet.http.HttpServletResponse; +import lombok.SneakyThrows; +import org.springblade.core.excel.listener.DataListener; +import org.springblade.core.excel.listener.ImportListener; +import org.springblade.core.excel.support.ExcelException; +import org.springblade.core.excel.support.ExcelImporter; +import org.springframework.util.StringUtils; +import org.springframework.web.multipart.MultipartFile; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Date; +import java.util.List; + +/** + * Excel工具类 + * + * @author Chill + * @apiNote https://easyexcel.opensource.alibaba.com/ + */ +public class ExcelUtil { + + /** + * 读取excel的所有sheet数据 + * + * @param excel excel文件 + * @return List + */ + public static List read(MultipartFile excel, Class clazz) { + DataListener dataListener = new DataListener<>(); + ExcelReaderBuilder builder = getReaderBuilder(excel, dataListener, clazz); + if (builder == null) { + return null; + } + builder.doReadAll(); + return dataListener.getDataList(); + } + + /** + * 读取excel的指定sheet数据 + * + * @param excel excel文件 + * @param sheetNo sheet序号(从0开始) + * @return List + */ + public static List read(MultipartFile excel, int sheetNo, Class clazz) { + return read(excel, sheetNo, 1, clazz); + } + + /** + * 读取excel的指定sheet数据 + * + * @param excel excel文件 + * @param sheetNo sheet序号(从0开始) + * @param headRowNumber 表头行数 + * @return List + */ + public static List read(MultipartFile excel, int sheetNo, int headRowNumber, Class clazz) { + DataListener dataListener = new DataListener<>(); + ExcelReaderBuilder builder = getReaderBuilder(excel, dataListener, clazz); + if (builder == null) { + return null; + } + builder.sheet(sheetNo).headRowNumber(headRowNumber).doRead(); + return dataListener.getDataList(); + } + + /** + * 读取并导入数据 + * + * @param excel excel文件 + * @param importer 导入逻辑类 + * @param 泛型 + */ + public static void save(MultipartFile excel, ExcelImporter importer, Class clazz) { + ImportListener importListener = new ImportListener<>(importer); + ExcelReaderBuilder builder = getReaderBuilder(excel, importListener, clazz); + if (builder != null) { + builder.doReadAll(); + } + } + + /** + * 导出excel + * + * @param response 响应类 + * @param dataList 数据列表 + * @param clazz class类 + * @param 泛型 + */ + @SneakyThrows + public static void export(HttpServletResponse response, List dataList, Class clazz) { + export(response, DateUtils.format(new Date(), DateUtils.DATE_FORMAT_14), "导出数据", dataList, clazz); + } + + /** + * 导出excel + * + * @param response 响应类 + * @param fileName 文件名 + * @param sheetName sheet名 + * @param dataList 数据列表 + * @param clazz class类 + * @param 泛型 + */ + @SneakyThrows + public static void export(HttpServletResponse response, String fileName, String sheetName, List dataList, Class clazz) { + export(response, DateUtils.format(new Date(), DateUtils.DATE_FORMAT_14), "导出数据", dataList, null, clazz); + } + + /** + * 导出excel + * + * @param response 响应类 + * @param fileName 文件名 + * @param sheetName sheet名 + * @param dataList 数据列表 + * @param clazz class类 + * @param writeHandler 自定义处理器 + * @param 泛型 + */ + @SneakyThrows + public static void export(HttpServletResponse response, String fileName, String sheetName, List dataList, WriteHandler writeHandler, Class clazz) { + response.setContentType("application/vnd.ms-excel"); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + fileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8); + response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx"); + + ExcelWriterBuilder write = EasyExcel.write(response.getOutputStream(), clazz); + if (writeHandler != null) { + write.registerWriteHandler(writeHandler); + } + write.sheet(sheetName).doWrite(dataList); + } + + /** + * 获取构建类 + * + * @param excel excel文件 + * @param readListener excel监听类 + * @return ExcelReaderBuilder + */ + public static ExcelReaderBuilder getReaderBuilder(MultipartFile excel, ReadListener readListener, Class clazz) { + String filename = excel.getOriginalFilename(); + if (!StringUtils.hasText(filename)) { + throw new ExcelException("请上传文件!"); + } + if ((!StringUtils.endsWithIgnoreCase(filename, ".xls") && !StringUtils.endsWithIgnoreCase(filename, ".xlsx"))) { + throw new ExcelException("请上传正确的excel文件!"); + } + InputStream inputStream; + try { + inputStream = new BufferedInputStream(excel.getInputStream()); + return EasyExcel.read(inputStream, clazz, readListener); + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } + +} diff --git a/blade-starter-flowable/pom.xml b/blade-starter-flowable/pom.xml new file mode 100644 index 0000000..5cccca4 --- /dev/null +++ b/blade-starter-flowable/pom.xml @@ -0,0 +1,35 @@ + + + 4.0.0 + + BladeX-Tool + org.springblade + ${revision} + + + blade-starter-flowable + ${project.artifactId} + ${project.parent.version} + jar + + + 7.0.1 + + + + + + org.flowable + flowable-spring-boot-starter + ${flowable.version} + + + net.tirasa.flowable-leftovers + flowable-json-converter + ${flowable.version} + + + + diff --git a/blade-starter-flowable/src/main/java/org/flowable/common/engine/impl/AbstractEngineConfiguration.java b/blade-starter-flowable/src/main/java/org/flowable/common/engine/impl/AbstractEngineConfiguration.java new file mode 100644 index 0000000..06d377a --- /dev/null +++ b/blade-starter-flowable/src/main/java/org/flowable/common/engine/impl/AbstractEngineConfiguration.java @@ -0,0 +1,2041 @@ +/* Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flowable.common.engine.impl; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import org.apache.commons.lang3.StringUtils; +import org.apache.ibatis.builder.xml.XMLConfigBuilder; +import org.apache.ibatis.builder.xml.XMLMapperBuilder; +import org.apache.ibatis.datasource.pooled.PooledDataSource; +import org.apache.ibatis.mapping.Environment; +import org.apache.ibatis.plugin.Interceptor; +import org.apache.ibatis.session.Configuration; +import org.apache.ibatis.session.SqlSessionFactory; +import org.apache.ibatis.session.defaults.DefaultSqlSessionFactory; +import org.apache.ibatis.transaction.TransactionFactory; +import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory; +import org.apache.ibatis.transaction.managed.ManagedTransactionFactory; +import org.apache.ibatis.type.*; +import org.flowable.common.engine.api.FlowableException; +import org.flowable.common.engine.api.delegate.event.FlowableEngineEventType; +import org.flowable.common.engine.api.delegate.event.FlowableEventDispatcher; +import org.flowable.common.engine.api.delegate.event.FlowableEventListener; +import org.flowable.common.engine.api.engine.EngineLifecycleListener; +import org.flowable.common.engine.impl.agenda.AgendaOperationExecutionListener; +import org.flowable.common.engine.impl.agenda.AgendaOperationRunner; +import org.flowable.common.engine.impl.cfg.CommandExecutorImpl; +import org.flowable.common.engine.impl.cfg.IdGenerator; +import org.flowable.common.engine.impl.cfg.TransactionContextFactory; +import org.flowable.common.engine.impl.cfg.standalone.StandaloneMybatisTransactionContextFactory; +import org.flowable.common.engine.impl.db.*; +import org.flowable.common.engine.impl.event.EventDispatchAction; +import org.flowable.common.engine.impl.event.FlowableEventDispatcherImpl; +import org.flowable.common.engine.impl.interceptor.*; +import org.flowable.common.engine.impl.lock.LockManager; +import org.flowable.common.engine.impl.lock.LockManagerImpl; +import org.flowable.common.engine.impl.logging.LoggingListener; +import org.flowable.common.engine.impl.logging.LoggingSession; +import org.flowable.common.engine.impl.logging.LoggingSessionFactory; +import org.flowable.common.engine.impl.persistence.GenericManagerFactory; +import org.flowable.common.engine.impl.persistence.StrongUuidGenerator; +import org.flowable.common.engine.impl.persistence.cache.EntityCache; +import org.flowable.common.engine.impl.persistence.cache.EntityCacheImpl; +import org.flowable.common.engine.impl.persistence.entity.*; +import org.flowable.common.engine.impl.persistence.entity.data.ByteArrayDataManager; +import org.flowable.common.engine.impl.persistence.entity.data.PropertyDataManager; +import org.flowable.common.engine.impl.persistence.entity.data.impl.MybatisByteArrayDataManager; +import org.flowable.common.engine.impl.persistence.entity.data.impl.MybatisPropertyDataManager; +import org.flowable.common.engine.impl.runtime.Clock; +import org.flowable.common.engine.impl.service.CommonEngineServiceImpl; +import org.flowable.common.engine.impl.util.DefaultClockImpl; +import org.flowable.common.engine.impl.util.IoUtil; +import org.flowable.common.engine.impl.util.ReflectUtil; +import org.flowable.eventregistry.api.EventRegistryEventConsumer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.naming.InitialContext; +import javax.sql.DataSource; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.sql.*; +import java.time.Duration; +import java.util.*; + +public abstract class AbstractEngineConfiguration { + + protected final Logger logger = LoggerFactory.getLogger(getClass()); + + /** The tenant id indicating 'no tenant' */ + public static final String NO_TENANT_ID = ""; + + /** + * Checks the version of the DB schema against the library when the form engine is being created and throws an exception if the versions don't match. + */ + public static final String DB_SCHEMA_UPDATE_FALSE = "false"; + public static final String DB_SCHEMA_UPDATE_CREATE = "create"; + public static final String DB_SCHEMA_UPDATE_CREATE_DROP = "create-drop"; + + /** + * Creates the schema when the form engine is being created and drops the schema when the form engine is being closed. + */ + public static final String DB_SCHEMA_UPDATE_DROP_CREATE = "drop-create"; + + /** + * Upon building of the process engine, a check is performed and an update of the schema is performed if it is necessary. + */ + public static final String DB_SCHEMA_UPDATE_TRUE = "true"; + + protected boolean forceCloseMybatisConnectionPool = true; + + protected String databaseType; + protected String jdbcDriver = "org.h2.Driver"; + protected String jdbcUrl = "jdbc:h2:tcp://localhost/~/flowable"; + protected String jdbcUsername = "sa"; + protected String jdbcPassword = ""; + protected String dataSourceJndiName; + protected int jdbcMaxActiveConnections = 16; + protected int jdbcMaxIdleConnections = 8; + protected int jdbcMaxCheckoutTime; + protected int jdbcMaxWaitTime; + protected boolean jdbcPingEnabled; + protected String jdbcPingQuery; + protected int jdbcPingConnectionNotUsedFor; + protected int jdbcDefaultTransactionIsolationLevel; + protected DataSource dataSource; + protected SchemaManager commonSchemaManager; + protected SchemaManager schemaManager; + protected Command schemaManagementCmd; + + protected String databaseSchemaUpdate = DB_SCHEMA_UPDATE_FALSE; + + /** + * Whether to use a lock when performing the database schema create or update operations. + */ + protected boolean useLockForDatabaseSchemaUpdate = false; + + protected String xmlEncoding = "UTF-8"; + + // COMMAND EXECUTORS /////////////////////////////////////////////// + + protected CommandExecutor commandExecutor; + protected Collection defaultCommandInterceptors; + protected CommandConfig defaultCommandConfig; + protected CommandConfig schemaCommandConfig; + protected CommandContextFactory commandContextFactory; + protected CommandInterceptor commandInvoker; + + protected AgendaOperationRunner agendaOperationRunner = (commandContext, runnable) -> runnable.run(); + protected Collection agendaOperationExecutionListeners; + + protected List customPreCommandInterceptors; + protected List customPostCommandInterceptors; + protected List commandInterceptors; + + protected Map engineConfigurations = new HashMap<>(); + protected Map serviceConfigurations = new HashMap<>(); + + protected ClassLoader classLoader; + /** + * Either use Class.forName or ClassLoader.loadClass for class loading. See http://forums.activiti.org/content/reflectutilloadclass-and-custom- classloader + */ + protected boolean useClassForNameClassLoading = true; + + protected List engineLifecycleListeners; + + // Event Registry ////////////////////////////////////////////////// + protected Map eventRegistryEventConsumers = new HashMap<>(); + + // MYBATIS SQL SESSION FACTORY ///////////////////////////////////// + + protected boolean isDbHistoryUsed = true; + protected DbSqlSessionFactory dbSqlSessionFactory; + protected SqlSessionFactory sqlSessionFactory; + protected TransactionFactory transactionFactory; + protected TransactionContextFactory transactionContextFactory; + + /** + * If set to true, enables bulk insert (grouping sql inserts together). Default true. + * For some databases (eg DB2+z/OS) needs to be set to false. + */ + protected boolean isBulkInsertEnabled = true; + + /** + * Some databases have a limit of how many parameters one sql insert can have (eg SQL Server, 2000 params (!= insert statements) ). Tweak this parameter in case of exceptions indicating too much + * is being put into one bulk insert, or make it higher if your database can cope with it and there are inserts with a huge amount of data. + *

+ * By default: 100 (55 for mssql server as it has a hard limit of 2000 parameters in a statement) + */ + protected int maxNrOfStatementsInBulkInsert = 100; + + public int DEFAULT_MAX_NR_OF_STATEMENTS_BULK_INSERT_SQL_SERVER = 55; // currently Execution has most params (35). 2000 / 35 = 57. + + protected String mybatisMappingFile; + protected Set> customMybatisMappers; + protected Set customMybatisXMLMappers; + protected List customMybatisInterceptors; + + protected Set dependentEngineMyBatisXmlMappers; + protected List dependentEngineMybatisTypeAliasConfigs; + protected List dependentEngineMybatisTypeHandlerConfigs; + + // SESSION FACTORIES /////////////////////////////////////////////// + protected List customSessionFactories; + protected Map, SessionFactory> sessionFactories; + + protected boolean enableEventDispatcher = true; + protected FlowableEventDispatcher eventDispatcher; + protected List eventListeners; + protected Map> typedEventListeners; + protected List additionalEventDispatchActions; + + protected LoggingListener loggingListener; + + protected boolean transactionsExternallyManaged; + + /** + * Flag that can be set to configure or not a relational database is used. This is useful for custom implementations that do not use relational databases at all. + * + * If true (default), the {@link AbstractEngineConfiguration#getDatabaseSchemaUpdate()} value will be used to determine what needs to happen wrt the database schema. + * + * If false, no validation or schema creation will be done. That means that the database schema must have been created 'manually' before but the engine does not validate whether the schema is + * correct. The {@link AbstractEngineConfiguration#getDatabaseSchemaUpdate()} value will not be used. + */ + protected boolean usingRelationalDatabase = true; + + /** + * Flag that can be set to configure whether or not a schema is used. This is useful for custom implementations that do not use relational databases at all. + * Setting {@link #usingRelationalDatabase} to true will automatically imply using a schema. + */ + protected boolean usingSchemaMgmt = true; + + /** + * Allows configuring a database table prefix which is used for all runtime operations of the process engine. For example, if you specify a prefix named 'PRE1.', Flowable will query for executions + * in a table named 'PRE1.ACT_RU_EXECUTION_'. + * + *

+ * NOTE: the prefix is not respected by automatic database schema management. If you use {@link AbstractEngineConfiguration#DB_SCHEMA_UPDATE_CREATE_DROP} or + * {@link AbstractEngineConfiguration#DB_SCHEMA_UPDATE_TRUE}, Flowable will create the database tables using the default names, regardless of the prefix configured here. + */ + protected String databaseTablePrefix = ""; + + /** + * Escape character for doing wildcard searches. + * + * This will be added at then end of queries that include for example a LIKE clause. For example: SELECT * FROM table WHERE column LIKE '%\%%' ESCAPE '\'; + */ + protected String databaseWildcardEscapeCharacter; + + /** + * database catalog to use + */ + protected String databaseCatalog = ""; + + /** + * In some situations you want to set the schema to use for table checks / generation if the database metadata doesn't return that correctly, see https://jira.codehaus.org/browse/ACT-1220, + * https://jira.codehaus.org/browse/ACT-1062 + */ + protected String databaseSchema; + + /** + * Set to true in case the defined databaseTablePrefix is a schema-name, instead of an actual table name prefix. This is relevant for checking if Flowable-tables exist, the databaseTablePrefix + * will not be used here - since the schema is taken into account already, adding a prefix for the table-check will result in wrong table-names. + */ + protected boolean tablePrefixIsSchema; + + /** + * Set to true if the latest version of a definition should be retrieved, ignoring a possible parent deployment id value + */ + protected boolean alwaysLookupLatestDefinitionVersion; + + /** + * Set to true if by default lookups should fallback to the default tenant (an empty string by default or a defined tenant value) + */ + protected boolean fallbackToDefaultTenant; + + /** + * Default tenant provider that is executed when looking up definitions, in case the global or local fallback to default tenant value is true + */ + protected DefaultTenantProvider defaultTenantProvider = (tenantId, scope, scopeKey) -> NO_TENANT_ID; + + /** + * Enables the MyBatis plugin that logs the execution time of sql statements. + */ + protected boolean enableLogSqlExecutionTime; + + protected Properties databaseTypeMappings = getDefaultDatabaseTypeMappings(); + + /** + * Duration between the checks when acquiring a lock. + */ + protected Duration lockPollRate = Duration.ofSeconds(10); + + /** + * Duration to wait for the DB Schema lock before giving up. + */ + protected Duration schemaLockWaitTime = Duration.ofMinutes(5); + + // DATA MANAGERS ////////////////////////////////////////////////////////////////// + + protected PropertyDataManager propertyDataManager; + protected ByteArrayDataManager byteArrayDataManager; + protected TableDataManager tableDataManager; + + // ENTITY MANAGERS //////////////////////////////////////////////////////////////// + + protected PropertyEntityManager propertyEntityManager; + protected ByteArrayEntityManager byteArrayEntityManager; + + protected List customPreDeployers; + protected List customPostDeployers; + protected List deployers; + + // CONFIGURATORS //////////////////////////////////////////////////////////// + + protected boolean enableConfiguratorServiceLoader = true; // Enabled by default. In certain environments this should be set to false (eg osgi) + protected List configurators; // The injected configurators + protected List allConfigurators; // Including auto-discovered configurators + protected EngineConfigurator idmEngineConfigurator; + protected EngineConfigurator eventRegistryConfigurator; + + public static final String PRODUCT_NAME_POSTGRES = "PostgreSQL"; + public static final String PRODUCT_NAME_CRDB = "CockroachDB"; + + public static final String DATABASE_TYPE_H2 = "h2"; + public static final String DATABASE_TYPE_HSQL = "hsql"; + public static final String DATABASE_TYPE_MYSQL = "mysql"; + public static final String DATABASE_TYPE_ORACLE = "oracle"; + public static final String DATABASE_TYPE_POSTGRES = "postgres"; + public static final String DATABASE_TYPE_MSSQL = "mssql"; + public static final String DATABASE_TYPE_DB2 = "db2"; + public static final String DATABASE_TYPE_COCKROACHDB = "cockroachdb"; + public static final String DATABASE_TYPE_DM = "oracle"; + public static final String DATABASE_TYPE_YASDB = "oracle"; + + public static Properties getDefaultDatabaseTypeMappings() { + Properties databaseTypeMappings = new Properties(); + databaseTypeMappings.setProperty("H2", DATABASE_TYPE_H2); + databaseTypeMappings.setProperty("HSQL Database Engine", DATABASE_TYPE_HSQL); + databaseTypeMappings.setProperty("MySQL", DATABASE_TYPE_MYSQL); + databaseTypeMappings.setProperty("MariaDB", DATABASE_TYPE_MYSQL); + databaseTypeMappings.setProperty("Oracle", DATABASE_TYPE_ORACLE); + databaseTypeMappings.setProperty(PRODUCT_NAME_POSTGRES, DATABASE_TYPE_POSTGRES); + databaseTypeMappings.setProperty("Microsoft SQL Server", DATABASE_TYPE_MSSQL); + databaseTypeMappings.setProperty(DATABASE_TYPE_DB2, DATABASE_TYPE_DB2); + databaseTypeMappings.setProperty("DB2", DATABASE_TYPE_DB2); + databaseTypeMappings.setProperty("DB2/NT", DATABASE_TYPE_DB2); + databaseTypeMappings.setProperty("DB2/NT64", DATABASE_TYPE_DB2); + databaseTypeMappings.setProperty("DB2 UDP", DATABASE_TYPE_DB2); + databaseTypeMappings.setProperty("DB2/LINUX", DATABASE_TYPE_DB2); + databaseTypeMappings.setProperty("DB2/LINUX390", DATABASE_TYPE_DB2); + databaseTypeMappings.setProperty("DB2/LINUXX8664", DATABASE_TYPE_DB2); + databaseTypeMappings.setProperty("DB2/LINUXZ64", DATABASE_TYPE_DB2); + databaseTypeMappings.setProperty("DB2/LINUXPPC64", DATABASE_TYPE_DB2); + databaseTypeMappings.setProperty("DB2/LINUXPPC64LE", DATABASE_TYPE_DB2); + databaseTypeMappings.setProperty("DB2/400 SQL", DATABASE_TYPE_DB2); + databaseTypeMappings.setProperty("DB2/6000", DATABASE_TYPE_DB2); + databaseTypeMappings.setProperty("DB2 UDB iSeries", DATABASE_TYPE_DB2); + databaseTypeMappings.setProperty("DB2/AIX64", DATABASE_TYPE_DB2); + databaseTypeMappings.setProperty("DB2/HPUX", DATABASE_TYPE_DB2); + databaseTypeMappings.setProperty("DB2/HP64", DATABASE_TYPE_DB2); + databaseTypeMappings.setProperty("DB2/SUN", DATABASE_TYPE_DB2); + databaseTypeMappings.setProperty("DB2/SUN64", DATABASE_TYPE_DB2); + databaseTypeMappings.setProperty("DB2/PTX", DATABASE_TYPE_DB2); + databaseTypeMappings.setProperty("DB2/2", DATABASE_TYPE_DB2); + databaseTypeMappings.setProperty("DB2 UDB AS400", DATABASE_TYPE_DB2); + databaseTypeMappings.setProperty("DM DBMS", DATABASE_TYPE_DM); + databaseTypeMappings.setProperty("YashanDB", DATABASE_TYPE_YASDB); + databaseTypeMappings.setProperty(PRODUCT_NAME_CRDB, DATABASE_TYPE_COCKROACHDB); + return databaseTypeMappings; + } + + protected Map beans; + + protected IdGenerator idGenerator; + protected boolean usePrefixId; + + protected Clock clock; + protected ObjectMapper objectMapper; + + // Variables + + public static final int DEFAULT_GENERIC_MAX_LENGTH_STRING = 4000; + public static final int DEFAULT_ORACLE_MAX_LENGTH_STRING = 2000; + + /** + * Define a max length for storing String variable types in the database. Mainly used for the Oracle NVARCHAR2 limit of 2000 characters + */ + protected int maxLengthStringVariableType = -1; + + protected void initEngineConfigurations() { + addEngineConfiguration(getEngineCfgKey(), getEngineScopeType(), this); + } + + // DataSource + // /////////////////////////////////////////////////////////////// + + protected void initDataSource() { + if (dataSource == null) { + if (dataSourceJndiName != null) { + try { + dataSource = (DataSource) new InitialContext().lookup(dataSourceJndiName); + } catch (Exception e) { + throw new FlowableException("couldn't lookup datasource from " + dataSourceJndiName + ": " + e.getMessage(), e); + } + + } else if (jdbcUrl != null) { + if ((jdbcDriver == null) || (jdbcUsername == null)) { + throw new FlowableException("DataSource or JDBC properties have to be specified in a process engine configuration"); + } + + logger.debug("initializing datasource to db: {}", jdbcUrl); + + if (logger.isInfoEnabled()) { + logger.info("Configuring Datasource with following properties (omitted password for security)"); + logger.info("datasource driver : {}", jdbcDriver); + logger.info("datasource url : {}", jdbcUrl); + logger.info("datasource user name : {}", jdbcUsername); + } + + PooledDataSource pooledDataSource = new PooledDataSource(this.getClass().getClassLoader(), jdbcDriver, jdbcUrl, jdbcUsername, jdbcPassword); + + if (jdbcMaxActiveConnections > 0) { + pooledDataSource.setPoolMaximumActiveConnections(jdbcMaxActiveConnections); + } + if (jdbcMaxIdleConnections > 0) { + pooledDataSource.setPoolMaximumIdleConnections(jdbcMaxIdleConnections); + } + if (jdbcMaxCheckoutTime > 0) { + pooledDataSource.setPoolMaximumCheckoutTime(jdbcMaxCheckoutTime); + } + if (jdbcMaxWaitTime > 0) { + pooledDataSource.setPoolTimeToWait(jdbcMaxWaitTime); + } + if (jdbcPingEnabled) { + pooledDataSource.setPoolPingEnabled(true); + if (jdbcPingQuery != null) { + pooledDataSource.setPoolPingQuery(jdbcPingQuery); + } + pooledDataSource.setPoolPingConnectionsNotUsedFor(jdbcPingConnectionNotUsedFor); + } + if (jdbcDefaultTransactionIsolationLevel > 0) { + pooledDataSource.setDefaultTransactionIsolationLevel(jdbcDefaultTransactionIsolationLevel); + } + dataSource = pooledDataSource; + } + } + + if (databaseType == null) { + initDatabaseType(); + } + } + + public void initDatabaseType() { + Connection connection = null; + try { + connection = dataSource.getConnection(); + DatabaseMetaData databaseMetaData = connection.getMetaData(); + String databaseProductName = databaseMetaData.getDatabaseProductName(); + logger.debug("database product name: '{}'", databaseProductName); + + // CRDB does not expose the version through the jdbc driver, so we need to fetch it through version(). + if (PRODUCT_NAME_POSTGRES.equalsIgnoreCase(databaseProductName)) { + try (PreparedStatement preparedStatement = connection.prepareStatement("select version() as version;"); + ResultSet resultSet = preparedStatement.executeQuery()) { + String version = null; + if (resultSet.next()) { + version = resultSet.getString("version"); + } + + if (StringUtils.isNotEmpty(version) && version.toLowerCase().startsWith(PRODUCT_NAME_CRDB.toLowerCase())) { + databaseProductName = PRODUCT_NAME_CRDB; + logger.info("CockroachDB version '{}' detected", version); + } + } + } + + databaseType = databaseTypeMappings.getProperty(databaseProductName); + if (databaseType == null) { + throw new FlowableException("couldn't deduct database type from database product name '" + databaseProductName + "'"); + } + logger.debug("using database type: {}", databaseType); + + } catch (SQLException e) { + throw new RuntimeException("Exception while initializing Database connection", e); + } finally { + try { + if (connection != null) { + connection.close(); + } + } catch (SQLException e) { + logger.error("Exception while closing the Database connection", e); + } + } + + // Special care for MSSQL, as it has a hard limit of 2000 params per statement (incl bulk statement). + // Especially with executions, with 100 as default, this limit is passed. + if (DATABASE_TYPE_MSSQL.equals(databaseType)) { + maxNrOfStatementsInBulkInsert = DEFAULT_MAX_NR_OF_STATEMENTS_BULK_INSERT_SQL_SERVER; + } + } + + public void initSchemaManager() { + if (this.commonSchemaManager == null) { + this.commonSchemaManager = new CommonDbSchemaManager(); + } + } + + // session factories //////////////////////////////////////////////////////// + + public void addSessionFactory(SessionFactory sessionFactory) { + sessionFactories.put(sessionFactory.getSessionType(), sessionFactory); + } + + public void initCommandContextFactory() { + if (commandContextFactory == null) { + commandContextFactory = new CommandContextFactory(); + } + } + + public void initTransactionContextFactory() { + if (transactionContextFactory == null) { + transactionContextFactory = new StandaloneMybatisTransactionContextFactory(); + } + } + + public void initCommandExecutors() { + initDefaultCommandConfig(); + initSchemaCommandConfig(); + initCommandInvoker(); + initCommandInterceptors(); + initCommandExecutor(); + } + + + public void initDefaultCommandConfig() { + if (defaultCommandConfig == null) { + defaultCommandConfig = new CommandConfig(); + } + } + + public void initSchemaCommandConfig() { + if (schemaCommandConfig == null) { + schemaCommandConfig = new CommandConfig(); + } + } + + public void initCommandInvoker() { + if (commandInvoker == null) { + commandInvoker = new DefaultCommandInvoker(); + } + } + + public void initCommandInterceptors() { + if (commandInterceptors == null) { + commandInterceptors = new ArrayList<>(); + if (customPreCommandInterceptors != null) { + commandInterceptors.addAll(customPreCommandInterceptors); + } + commandInterceptors.addAll(getDefaultCommandInterceptors()); + if (customPostCommandInterceptors != null) { + commandInterceptors.addAll(customPostCommandInterceptors); + } + commandInterceptors.add(commandInvoker); + } + } + + public Collection getDefaultCommandInterceptors() { + if (defaultCommandInterceptors == null) { + List interceptors = new ArrayList<>(); + interceptors.add(new LogInterceptor()); + + if (DATABASE_TYPE_COCKROACHDB.equals(databaseType)) { + interceptors.add(new CrDbRetryInterceptor()); + } + + CommandInterceptor transactionInterceptor = createTransactionInterceptor(); + if (transactionInterceptor != null) { + interceptors.add(transactionInterceptor); + } + + if (commandContextFactory != null) { + String engineCfgKey = getEngineCfgKey(); + CommandContextInterceptor commandContextInterceptor = new CommandContextInterceptor(commandContextFactory, + classLoader, useClassForNameClassLoading, clock, objectMapper); + engineConfigurations.put(engineCfgKey, this); + commandContextInterceptor.setEngineCfgKey(engineCfgKey); + commandContextInterceptor.setEngineConfigurations(engineConfigurations); + interceptors.add(commandContextInterceptor); + } + + if (transactionContextFactory != null) { + interceptors.add(new TransactionContextInterceptor(transactionContextFactory)); + } + + List additionalCommandInterceptors = getAdditionalDefaultCommandInterceptors(); + if (additionalCommandInterceptors != null) { + interceptors.addAll(additionalCommandInterceptors); + } + + defaultCommandInterceptors = interceptors; + } + return defaultCommandInterceptors; + } + + public abstract String getEngineCfgKey(); + + public abstract String getEngineScopeType(); + + public List getAdditionalDefaultCommandInterceptors() { + return null; + } + + public void initCommandExecutor() { + if (commandExecutor == null) { + CommandInterceptor first = initInterceptorChain(commandInterceptors); + commandExecutor = new CommandExecutorImpl(getDefaultCommandConfig(), first); + } + } + + public CommandInterceptor initInterceptorChain(List chain) { + if (chain == null || chain.isEmpty()) { + throw new FlowableException("invalid command interceptor chain configuration: " + chain); + } + for (int i = 0; i < chain.size() - 1; i++) { + chain.get(i).setNext(chain.get(i + 1)); + } + return chain.get(0); + } + + public abstract CommandInterceptor createTransactionInterceptor(); + + + public void initBeans() { + if (beans == null) { + beans = new HashMap<>(); + } + } + + // id generator + // ///////////////////////////////////////////////////////////// + + public void initIdGenerator() { + if (idGenerator == null) { + idGenerator = new StrongUuidGenerator(); + } + } + + public void initObjectMapper() { + if (objectMapper == null) { + objectMapper = new ObjectMapper(); + objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); + } + } + + public void initClock() { + if (clock == null) { + clock = new DefaultClockImpl(); + } + } + + // Data managers /////////////////////////////////////////////////////////// + + public void initDataManagers() { + if (propertyDataManager == null) { + propertyDataManager = new MybatisPropertyDataManager(idGenerator); + } + + if (byteArrayDataManager == null) { + byteArrayDataManager = new MybatisByteArrayDataManager(idGenerator); + } + } + + // Entity managers ////////////////////////////////////////////////////////// + + public void initEntityManagers() { + if (propertyEntityManager == null) { + propertyEntityManager = new PropertyEntityManagerImpl(this, propertyDataManager); + } + + if (byteArrayEntityManager == null) { + byteArrayEntityManager = new ByteArrayEntityManagerImpl(byteArrayDataManager, getEngineCfgKey(), this::getEventDispatcher); + } + + if (tableDataManager == null) { + tableDataManager = new TableDataManagerImpl(this); + } + } + + // services + // ///////////////////////////////////////////////////////////////// + + protected void initService(Object service) { + if (service instanceof CommonEngineServiceImpl) { + ((CommonEngineServiceImpl) service).setCommandExecutor(commandExecutor); + } + } + + // myBatis SqlSessionFactory + // //////////////////////////////////////////////// + + public void initSessionFactories() { + if (sessionFactories == null) { + sessionFactories = new HashMap<>(); + + if (usingRelationalDatabase) { + initDbSqlSessionFactory(); + } + + addSessionFactory(new GenericManagerFactory(EntityCache.class, EntityCacheImpl.class)); + + if (isLoggingSessionEnabled()) { + if (!sessionFactories.containsKey(LoggingSession.class)) { + LoggingSessionFactory loggingSessionFactory = new LoggingSessionFactory(); + loggingSessionFactory.setLoggingListener(loggingListener); + loggingSessionFactory.setObjectMapper(objectMapper); + sessionFactories.put(LoggingSession.class, loggingSessionFactory); + } + } + + commandContextFactory.setSessionFactories(sessionFactories); + + } else { + if (usingRelationalDatabase) { + initDbSqlSessionFactoryEntitySettings(); + } + } + + if (customSessionFactories != null) { + for (SessionFactory sessionFactory : customSessionFactories) { + addSessionFactory(sessionFactory); + } + } + } + + public void initDbSqlSessionFactory() { + if (dbSqlSessionFactory == null) { + dbSqlSessionFactory = createDbSqlSessionFactory(); + } + dbSqlSessionFactory.setDatabaseType(databaseType); + dbSqlSessionFactory.setSqlSessionFactory(sqlSessionFactory); + dbSqlSessionFactory.setDbHistoryUsed(isDbHistoryUsed); + dbSqlSessionFactory.setDatabaseTablePrefix(databaseTablePrefix); + dbSqlSessionFactory.setTablePrefixIsSchema(tablePrefixIsSchema); + dbSqlSessionFactory.setDatabaseCatalog(databaseCatalog); + dbSqlSessionFactory.setDatabaseSchema(databaseSchema); + dbSqlSessionFactory.setMaxNrOfStatementsInBulkInsert(maxNrOfStatementsInBulkInsert); + + initDbSqlSessionFactoryEntitySettings(); + + addSessionFactory(dbSqlSessionFactory); + } + + public DbSqlSessionFactory createDbSqlSessionFactory() { + return new DbSqlSessionFactory(usePrefixId); + } + + protected abstract void initDbSqlSessionFactoryEntitySettings(); + + protected void defaultInitDbSqlSessionFactoryEntitySettings(List> insertOrder, List> deleteOrder) { + if (insertOrder != null) { + for (Class clazz : insertOrder) { + dbSqlSessionFactory.getInsertionOrder().add(clazz); + + if (isBulkInsertEnabled) { + dbSqlSessionFactory.getBulkInserteableEntityClasses().add(clazz); + } + } + } + + if (deleteOrder != null) { + for (Class clazz : deleteOrder) { + dbSqlSessionFactory.getDeletionOrder().add(clazz); + } + } + } + + public void initTransactionFactory() { + if (transactionFactory == null) { + if (transactionsExternallyManaged) { + transactionFactory = new ManagedTransactionFactory(); + Properties properties = new Properties(); + properties.put("closeConnection", "false"); + this.transactionFactory.setProperties(properties); + } else { + transactionFactory = new JdbcTransactionFactory(); + } + } + } + + public void initSqlSessionFactory() { + if (sqlSessionFactory == null) { + InputStream inputStream = null; + try { + inputStream = getMyBatisXmlConfigurationStream(); + + Environment environment = new Environment("default", transactionFactory, dataSource); + Reader reader = new InputStreamReader(inputStream); + Properties properties = new Properties(); + properties.put("prefix", databaseTablePrefix); + + String wildcardEscapeClause = ""; + if ((databaseWildcardEscapeCharacter != null) && (databaseWildcardEscapeCharacter.length() != 0)) { + wildcardEscapeClause = " escape '" + databaseWildcardEscapeCharacter + "'"; + } + properties.put("wildcardEscapeClause", wildcardEscapeClause); + + // set default properties + properties.put("limitBefore", ""); + properties.put("limitAfter", ""); + properties.put("limitBetween", ""); + properties.put("limitBeforeNativeQuery", ""); + properties.put("limitAfterNativeQuery", ""); + properties.put("blobType", "BLOB"); + properties.put("boolValue", "TRUE"); + + if (databaseType != null) { + properties.load(getResourceAsStream(pathToEngineDbProperties())); + } + + Configuration configuration = initMybatisConfiguration(environment, reader, properties); + sqlSessionFactory = new DefaultSqlSessionFactory(configuration); + + } catch (Exception e) { + throw new FlowableException("Error while building ibatis SqlSessionFactory: " + e.getMessage(), e); + } finally { + IoUtil.closeSilently(inputStream); + } + } else { + // This is needed when the SQL Session Factory is created by another engine. + // When custom XML Mappers are registered with this engine they need to be loaded in the configuration as well + applyCustomMybatisCustomizations(sqlSessionFactory.getConfiguration()); + } + } + + public String pathToEngineDbProperties() { + return "org/flowable/common/db/properties/" + databaseType + ".properties"; + } + + public Configuration initMybatisConfiguration(Environment environment, Reader reader, Properties properties) { + XMLConfigBuilder parser = new XMLConfigBuilder(reader, "", properties); + Configuration configuration = parser.getConfiguration(); + + if (databaseType != null) { + configuration.setDatabaseId(databaseType); + } + + configuration.setEnvironment(environment); + + initMybatisTypeHandlers(configuration); + initCustomMybatisInterceptors(configuration); + if (isEnableLogSqlExecutionTime()) { + initMyBatisLogSqlExecutionTimePlugin(configuration); + } + + configuration = parseMybatisConfiguration(parser); + return configuration; + } + + public void initCustomMybatisMappers(Configuration configuration) { + if (getCustomMybatisMappers() != null) { + for (Class clazz : getCustomMybatisMappers()) { + if (!configuration.hasMapper(clazz)) { + configuration.addMapper(clazz); + } + } + } + } + + public void initMybatisTypeHandlers(Configuration configuration) { + // When mapping into Map there is currently a problem with MyBatis. + // It will return objects which are driver specific. + // Therefore we are registering the mappings between Object.class and the specific jdbc type here. + // see https://github.com/mybatis/mybatis-3/issues/2216 for more info + TypeHandlerRegistry handlerRegistry = configuration.getTypeHandlerRegistry(); + + handlerRegistry.register(Object.class, JdbcType.BOOLEAN, new BooleanTypeHandler()); + handlerRegistry.register(Object.class, JdbcType.BIT, new BooleanTypeHandler()); + + handlerRegistry.register(Object.class, JdbcType.TINYINT, new ByteTypeHandler()); + + handlerRegistry.register(Object.class, JdbcType.SMALLINT, new ShortTypeHandler()); + + handlerRegistry.register(Object.class, JdbcType.INTEGER, new IntegerTypeHandler()); + + handlerRegistry.register(Object.class, JdbcType.FLOAT, new FloatTypeHandler()); + + handlerRegistry.register(Object.class, JdbcType.DOUBLE, new DoubleTypeHandler()); + + handlerRegistry.register(Object.class, JdbcType.CHAR, new StringTypeHandler()); + handlerRegistry.register(Object.class, JdbcType.CLOB, new ClobTypeHandler()); + handlerRegistry.register(Object.class, JdbcType.VARCHAR, new StringTypeHandler()); + handlerRegistry.register(Object.class, JdbcType.LONGVARCHAR, new StringTypeHandler()); + handlerRegistry.register(Object.class, JdbcType.NVARCHAR, new NStringTypeHandler()); + handlerRegistry.register(Object.class, JdbcType.NCHAR, new NStringTypeHandler()); + handlerRegistry.register(Object.class, JdbcType.NCLOB, new NClobTypeHandler()); + + handlerRegistry.register(Object.class, JdbcType.ARRAY, new ArrayTypeHandler()); + + handlerRegistry.register(Object.class, JdbcType.BIGINT, new LongTypeHandler()); + + handlerRegistry.register(Object.class, JdbcType.REAL, new BigDecimalTypeHandler()); + handlerRegistry.register(Object.class, JdbcType.DECIMAL, new BigDecimalTypeHandler()); + handlerRegistry.register(Object.class, JdbcType.NUMERIC, new BigDecimalTypeHandler()); + + handlerRegistry.register(Object.class, JdbcType.BLOB, new BlobInputStreamTypeHandler()); + handlerRegistry.register(Object.class, JdbcType.LONGVARBINARY, new BlobTypeHandler()); + + handlerRegistry.register(Object.class, JdbcType.DATE, new DateOnlyTypeHandler()); + handlerRegistry.register(Object.class, JdbcType.TIME, new TimeOnlyTypeHandler()); + handlerRegistry.register(Object.class, JdbcType.TIMESTAMP, new DateTypeHandler()); + + handlerRegistry.register(Object.class, JdbcType.SQLXML, new SqlxmlTypeHandler()); + } + + public void initCustomMybatisInterceptors(Configuration configuration) { + if (customMybatisInterceptors!=null){ + for (Interceptor interceptor :customMybatisInterceptors){ + configuration.addInterceptor(interceptor); + } + } + } + + public void initMyBatisLogSqlExecutionTimePlugin(Configuration configuration) { + configuration.addInterceptor(new LogSqlExecutionTimePlugin()); + } + + public Configuration parseMybatisConfiguration(XMLConfigBuilder parser) { + Configuration configuration = parser.parse(); + + applyCustomMybatisCustomizations(configuration); + return configuration; + } + + protected void applyCustomMybatisCustomizations(Configuration configuration) { + initCustomMybatisMappers(configuration); + + if (dependentEngineMybatisTypeAliasConfigs != null) { + for (MybatisTypeAliasConfigurator typeAliasConfig : dependentEngineMybatisTypeAliasConfigs) { + typeAliasConfig.configure(configuration.getTypeAliasRegistry()); + } + } + if (dependentEngineMybatisTypeHandlerConfigs != null) { + for (MybatisTypeHandlerConfigurator typeHandlerConfig : dependentEngineMybatisTypeHandlerConfigs) { + typeHandlerConfig.configure(configuration.getTypeHandlerRegistry()); + } + } + + parseDependentEngineMybatisXMLMappers(configuration); + parseCustomMybatisXMLMappers(configuration); + } + + public void parseCustomMybatisXMLMappers(Configuration configuration) { + if (getCustomMybatisXMLMappers() != null) { + for (String resource : getCustomMybatisXMLMappers()) { + parseMybatisXmlMapping(configuration, resource); + } + } + } + + public void parseDependentEngineMybatisXMLMappers(Configuration configuration) { + if (getDependentEngineMyBatisXmlMappers() != null) { + for (String resource : getDependentEngineMyBatisXmlMappers()) { + parseMybatisXmlMapping(configuration, resource); + } + } + } + + protected void parseMybatisXmlMapping(Configuration configuration, String resource) { + // see XMLConfigBuilder.mapperElement() + XMLMapperBuilder mapperParser = new XMLMapperBuilder(getResourceAsStream(resource), configuration, resource, configuration.getSqlFragments()); + mapperParser.parse(); + } + + protected InputStream getResourceAsStream(String resource) { + ClassLoader classLoader = getClassLoader(); + if (classLoader != null) { + return getClassLoader().getResourceAsStream(resource); + } else { + return this.getClass().getClassLoader().getResourceAsStream(resource); + } + } + + public void setMybatisMappingFile(String file) { + this.mybatisMappingFile = file; + } + + public String getMybatisMappingFile() { + return mybatisMappingFile; + } + + public abstract InputStream getMyBatisXmlConfigurationStream(); + + public void initConfigurators() { + + allConfigurators = new ArrayList<>(); + allConfigurators.addAll(getEngineSpecificEngineConfigurators()); + + // Configurators that are explicitly added to the config + if (configurators != null) { + allConfigurators.addAll(configurators); + } + + // Auto discovery through ServiceLoader + if (enableConfiguratorServiceLoader) { + ClassLoader classLoader = getClassLoader(); + if (classLoader == null) { + classLoader = ReflectUtil.getClassLoader(); + } + + ServiceLoader configuratorServiceLoader = ServiceLoader.load(EngineConfigurator.class, classLoader); + int nrOfServiceLoadedConfigurators = 0; + for (EngineConfigurator configurator : configuratorServiceLoader) { + allConfigurators.add(configurator); + nrOfServiceLoadedConfigurators++; + } + + if (nrOfServiceLoadedConfigurators > 0) { + logger.info("Found {} auto-discoverable Process Engine Configurator{}", nrOfServiceLoadedConfigurators, nrOfServiceLoadedConfigurators > 1 ? "s" : ""); + } + + if (!allConfigurators.isEmpty()) { + + // Order them according to the priorities (useful for dependent + // configurator) + allConfigurators.sort(new Comparator() { + + @Override + public int compare(EngineConfigurator configurator1, EngineConfigurator configurator2) { + int priority1 = configurator1.getPriority(); + int priority2 = configurator2.getPriority(); + + if (priority1 < priority2) { + return -1; + } else if (priority1 > priority2) { + return 1; + } + return 0; + } + }); + + // Execute the configurators + logger.info("Found {} Engine Configurators in total:", allConfigurators.size()); + for (EngineConfigurator configurator : allConfigurators) { + logger.info("{} (priority:{})", configurator.getClass(), configurator.getPriority()); + } + + } + + } + } + + public void close() { + if (forceCloseMybatisConnectionPool && dataSource instanceof PooledDataSource) { + /* + * When the datasource is created by a Flowable engine (i.e. it's an instance of PooledDataSource), + * the connection pool needs to be closed when closing the engine. + * Note that calling forceCloseAll() multiple times (as is the case when running with multiple engine) is ok. + */ + ((PooledDataSource) dataSource).forceCloseAll(); + } + } + + protected List getEngineSpecificEngineConfigurators() { + // meant to be overridden if needed + return Collections.emptyList(); + } + + public void configuratorsBeforeInit() { + for (EngineConfigurator configurator : allConfigurators) { + logger.info("Executing beforeInit() of {} (priority:{})", configurator.getClass(), configurator.getPriority()); + configurator.beforeInit(this); + } + } + + public void configuratorsAfterInit() { + for (EngineConfigurator configurator : allConfigurators) { + logger.info("Executing configure() of {} (priority:{})", configurator.getClass(), configurator.getPriority()); + configurator.configure(this); + } + } + + public LockManager getLockManager(String lockName) { + return new LockManagerImpl(commandExecutor, lockName, getLockPollRate(), getEngineCfgKey()); + } + + // getters and setters + // ////////////////////////////////////////////////////// + + public abstract String getEngineName(); + + public ClassLoader getClassLoader() { + return classLoader; + } + + public AbstractEngineConfiguration setClassLoader(ClassLoader classLoader) { + this.classLoader = classLoader; + return this; + } + + public boolean isUseClassForNameClassLoading() { + return useClassForNameClassLoading; + } + + public AbstractEngineConfiguration setUseClassForNameClassLoading(boolean useClassForNameClassLoading) { + this.useClassForNameClassLoading = useClassForNameClassLoading; + return this; + } + + public void addEngineLifecycleListener(EngineLifecycleListener engineLifecycleListener) { + if (this.engineLifecycleListeners == null) { + this.engineLifecycleListeners = new ArrayList<>(); + } + this.engineLifecycleListeners.add(engineLifecycleListener); + } + + public List getEngineLifecycleListeners() { + return engineLifecycleListeners; + } + + public AbstractEngineConfiguration setEngineLifecycleListeners(List engineLifecycleListeners) { + this.engineLifecycleListeners = engineLifecycleListeners; + return this; + } + + public String getDatabaseType() { + return databaseType; + } + + public AbstractEngineConfiguration setDatabaseType(String databaseType) { + this.databaseType = databaseType; + return this; + } + + public DataSource getDataSource() { + return dataSource; + } + + public AbstractEngineConfiguration setDataSource(DataSource dataSource) { + this.dataSource = dataSource; + return this; + } + + public SchemaManager getSchemaManager() { + return schemaManager; + } + + public AbstractEngineConfiguration setSchemaManager(SchemaManager schemaManager) { + this.schemaManager = schemaManager; + return this; + } + + public SchemaManager getCommonSchemaManager() { + return commonSchemaManager; + } + + public AbstractEngineConfiguration setCommonSchemaManager(SchemaManager commonSchemaManager) { + this.commonSchemaManager = commonSchemaManager; + return this; + } + + public Command getSchemaManagementCmd() { + return schemaManagementCmd; + } + + public AbstractEngineConfiguration setSchemaManagementCmd(Command schemaManagementCmd) { + this.schemaManagementCmd = schemaManagementCmd; + return this; + } + + public String getJdbcDriver() { + return jdbcDriver; + } + + public AbstractEngineConfiguration setJdbcDriver(String jdbcDriver) { + this.jdbcDriver = jdbcDriver; + return this; + } + + public String getJdbcUrl() { + return jdbcUrl; + } + + public AbstractEngineConfiguration setJdbcUrl(String jdbcUrl) { + this.jdbcUrl = jdbcUrl; + return this; + } + + public String getJdbcUsername() { + return jdbcUsername; + } + + public AbstractEngineConfiguration setJdbcUsername(String jdbcUsername) { + this.jdbcUsername = jdbcUsername; + return this; + } + + public String getJdbcPassword() { + return jdbcPassword; + } + + public AbstractEngineConfiguration setJdbcPassword(String jdbcPassword) { + this.jdbcPassword = jdbcPassword; + return this; + } + + public int getJdbcMaxActiveConnections() { + return jdbcMaxActiveConnections; + } + + public AbstractEngineConfiguration setJdbcMaxActiveConnections(int jdbcMaxActiveConnections) { + this.jdbcMaxActiveConnections = jdbcMaxActiveConnections; + return this; + } + + public int getJdbcMaxIdleConnections() { + return jdbcMaxIdleConnections; + } + + public AbstractEngineConfiguration setJdbcMaxIdleConnections(int jdbcMaxIdleConnections) { + this.jdbcMaxIdleConnections = jdbcMaxIdleConnections; + return this; + } + + public int getJdbcMaxCheckoutTime() { + return jdbcMaxCheckoutTime; + } + + public AbstractEngineConfiguration setJdbcMaxCheckoutTime(int jdbcMaxCheckoutTime) { + this.jdbcMaxCheckoutTime = jdbcMaxCheckoutTime; + return this; + } + + public int getJdbcMaxWaitTime() { + return jdbcMaxWaitTime; + } + + public AbstractEngineConfiguration setJdbcMaxWaitTime(int jdbcMaxWaitTime) { + this.jdbcMaxWaitTime = jdbcMaxWaitTime; + return this; + } + + public boolean isJdbcPingEnabled() { + return jdbcPingEnabled; + } + + public AbstractEngineConfiguration setJdbcPingEnabled(boolean jdbcPingEnabled) { + this.jdbcPingEnabled = jdbcPingEnabled; + return this; + } + + public int getJdbcPingConnectionNotUsedFor() { + return jdbcPingConnectionNotUsedFor; + } + + public AbstractEngineConfiguration setJdbcPingConnectionNotUsedFor(int jdbcPingConnectionNotUsedFor) { + this.jdbcPingConnectionNotUsedFor = jdbcPingConnectionNotUsedFor; + return this; + } + + public int getJdbcDefaultTransactionIsolationLevel() { + return jdbcDefaultTransactionIsolationLevel; + } + + public AbstractEngineConfiguration setJdbcDefaultTransactionIsolationLevel(int jdbcDefaultTransactionIsolationLevel) { + this.jdbcDefaultTransactionIsolationLevel = jdbcDefaultTransactionIsolationLevel; + return this; + } + + public String getJdbcPingQuery() { + return jdbcPingQuery; + } + + public AbstractEngineConfiguration setJdbcPingQuery(String jdbcPingQuery) { + this.jdbcPingQuery = jdbcPingQuery; + return this; + } + + public String getDataSourceJndiName() { + return dataSourceJndiName; + } + + public AbstractEngineConfiguration setDataSourceJndiName(String dataSourceJndiName) { + this.dataSourceJndiName = dataSourceJndiName; + return this; + } + + public CommandConfig getSchemaCommandConfig() { + return schemaCommandConfig; + } + + public AbstractEngineConfiguration setSchemaCommandConfig(CommandConfig schemaCommandConfig) { + this.schemaCommandConfig = schemaCommandConfig; + return this; + } + + public boolean isTransactionsExternallyManaged() { + return transactionsExternallyManaged; + } + + public AbstractEngineConfiguration setTransactionsExternallyManaged(boolean transactionsExternallyManaged) { + this.transactionsExternallyManaged = transactionsExternallyManaged; + return this; + } + + public Map getBeans() { + return beans; + } + + public AbstractEngineConfiguration setBeans(Map beans) { + this.beans = beans; + return this; + } + + public IdGenerator getIdGenerator() { + return idGenerator; + } + + public AbstractEngineConfiguration setIdGenerator(IdGenerator idGenerator) { + this.idGenerator = idGenerator; + return this; + } + + public boolean isUsePrefixId() { + return usePrefixId; + } + + public AbstractEngineConfiguration setUsePrefixId(boolean usePrefixId) { + this.usePrefixId = usePrefixId; + return this; + } + + public String getXmlEncoding() { + return xmlEncoding; + } + + public AbstractEngineConfiguration setXmlEncoding(String xmlEncoding) { + this.xmlEncoding = xmlEncoding; + return this; + } + + public CommandConfig getDefaultCommandConfig() { + return defaultCommandConfig; + } + + public AbstractEngineConfiguration setDefaultCommandConfig(CommandConfig defaultCommandConfig) { + this.defaultCommandConfig = defaultCommandConfig; + return this; + } + + public CommandExecutor getCommandExecutor() { + return commandExecutor; + } + + public AbstractEngineConfiguration setCommandExecutor(CommandExecutor commandExecutor) { + this.commandExecutor = commandExecutor; + return this; + } + + public CommandContextFactory getCommandContextFactory() { + return commandContextFactory; + } + + public AbstractEngineConfiguration setCommandContextFactory(CommandContextFactory commandContextFactory) { + this.commandContextFactory = commandContextFactory; + return this; + } + + public CommandInterceptor getCommandInvoker() { + return commandInvoker; + } + + public AbstractEngineConfiguration setCommandInvoker(CommandInterceptor commandInvoker) { + this.commandInvoker = commandInvoker; + return this; + } + + public AgendaOperationRunner getAgendaOperationRunner() { + return agendaOperationRunner; + } + + public AbstractEngineConfiguration setAgendaOperationRunner(AgendaOperationRunner agendaOperationRunner) { + this.agendaOperationRunner = agendaOperationRunner; + return this; + } + + public Collection getAgendaOperationExecutionListeners() { + return agendaOperationExecutionListeners; + } + + public AbstractEngineConfiguration addAgendaOperationExecutionListener(AgendaOperationExecutionListener listener) { + if (this.agendaOperationExecutionListeners == null) { + this.agendaOperationExecutionListeners = new ArrayList<>(); + } + this.agendaOperationExecutionListeners.add(listener); + return this; + } + + public AbstractEngineConfiguration setAgendaOperationExecutionListeners(Collection agendaOperationExecutionListeners) { + this.agendaOperationExecutionListeners = agendaOperationExecutionListeners; + return this; + } + + public List getCustomPreCommandInterceptors() { + return customPreCommandInterceptors; + } + + public AbstractEngineConfiguration addCustomPreCommandInterceptor(CommandInterceptor commandInterceptor) { + if (this.customPreCommandInterceptors == null) { + this.customPreCommandInterceptors = new ArrayList<>(); + } + this.customPreCommandInterceptors.add(commandInterceptor); + return this; + } + + public AbstractEngineConfiguration setCustomPreCommandInterceptors(List customPreCommandInterceptors) { + this.customPreCommandInterceptors = customPreCommandInterceptors; + return this; + } + + public List getCustomPostCommandInterceptors() { + return customPostCommandInterceptors; + } + + public AbstractEngineConfiguration addCustomPostCommandInterceptor(CommandInterceptor commandInterceptor) { + if (this.customPostCommandInterceptors == null) { + this.customPostCommandInterceptors = new ArrayList<>(); + } + this.customPostCommandInterceptors.add(commandInterceptor); + return this; + } + + public AbstractEngineConfiguration setCustomPostCommandInterceptors(List customPostCommandInterceptors) { + this.customPostCommandInterceptors = customPostCommandInterceptors; + return this; + } + + public List getCommandInterceptors() { + return commandInterceptors; + } + + public AbstractEngineConfiguration setCommandInterceptors(List commandInterceptors) { + this.commandInterceptors = commandInterceptors; + return this; + } + + public Map getEngineConfigurations() { + return engineConfigurations; + } + + public AbstractEngineConfiguration setEngineConfigurations(Map engineConfigurations) { + this.engineConfigurations = engineConfigurations; + return this; + } + + public void addEngineConfiguration(String key, String scopeType, AbstractEngineConfiguration engineConfiguration) { + if (engineConfigurations == null) { + engineConfigurations = new HashMap<>(); + } + engineConfigurations.put(key, engineConfiguration); + engineConfigurations.put(scopeType, engineConfiguration); + } + + public Map getServiceConfigurations() { + return serviceConfigurations; + } + + public AbstractEngineConfiguration setServiceConfigurations(Map serviceConfigurations) { + this.serviceConfigurations = serviceConfigurations; + return this; + } + + public void addServiceConfiguration(String key, AbstractServiceConfiguration serviceConfiguration) { + if (serviceConfigurations == null) { + serviceConfigurations = new HashMap<>(); + } + serviceConfigurations.put(key, serviceConfiguration); + } + + public Map getEventRegistryEventConsumers() { + return eventRegistryEventConsumers; + } + + public AbstractEngineConfiguration setEventRegistryEventConsumers(Map eventRegistryEventConsumers) { + this.eventRegistryEventConsumers = eventRegistryEventConsumers; + return this; + } + + public void addEventRegistryEventConsumer(String key, EventRegistryEventConsumer eventRegistryEventConsumer) { + if (eventRegistryEventConsumers == null) { + eventRegistryEventConsumers = new HashMap<>(); + } + eventRegistryEventConsumers.put(key, eventRegistryEventConsumer); + } + + public AbstractEngineConfiguration setDefaultCommandInterceptors(Collection defaultCommandInterceptors) { + this.defaultCommandInterceptors = defaultCommandInterceptors; + return this; + } + + public SqlSessionFactory getSqlSessionFactory() { + return sqlSessionFactory; + } + + public AbstractEngineConfiguration setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) { + this.sqlSessionFactory = sqlSessionFactory; + return this; + } + + public boolean isDbHistoryUsed() { + return isDbHistoryUsed; + } + + public AbstractEngineConfiguration setDbHistoryUsed(boolean isDbHistoryUsed) { + this.isDbHistoryUsed = isDbHistoryUsed; + return this; + } + + public DbSqlSessionFactory getDbSqlSessionFactory() { + return dbSqlSessionFactory; + } + + public AbstractEngineConfiguration setDbSqlSessionFactory(DbSqlSessionFactory dbSqlSessionFactory) { + this.dbSqlSessionFactory = dbSqlSessionFactory; + return this; + } + + public TransactionFactory getTransactionFactory() { + return transactionFactory; + } + + public AbstractEngineConfiguration setTransactionFactory(TransactionFactory transactionFactory) { + this.transactionFactory = transactionFactory; + return this; + } + + public TransactionContextFactory getTransactionContextFactory() { + return transactionContextFactory; + } + + public AbstractEngineConfiguration setTransactionContextFactory(TransactionContextFactory transactionContextFactory) { + this.transactionContextFactory = transactionContextFactory; + return this; + } + + public int getMaxNrOfStatementsInBulkInsert() { + return maxNrOfStatementsInBulkInsert; + } + + public AbstractEngineConfiguration setMaxNrOfStatementsInBulkInsert(int maxNrOfStatementsInBulkInsert) { + this.maxNrOfStatementsInBulkInsert = maxNrOfStatementsInBulkInsert; + return this; + } + + public boolean isBulkInsertEnabled() { + return isBulkInsertEnabled; + } + + public AbstractEngineConfiguration setBulkInsertEnabled(boolean isBulkInsertEnabled) { + this.isBulkInsertEnabled = isBulkInsertEnabled; + return this; + } + + public Set> getCustomMybatisMappers() { + return customMybatisMappers; + } + + public AbstractEngineConfiguration setCustomMybatisMappers(Set> customMybatisMappers) { + this.customMybatisMappers = customMybatisMappers; + return this; + } + + public Set getCustomMybatisXMLMappers() { + return customMybatisXMLMappers; + } + + public AbstractEngineConfiguration setCustomMybatisXMLMappers(Set customMybatisXMLMappers) { + this.customMybatisXMLMappers = customMybatisXMLMappers; + return this; + } + + public Set getDependentEngineMyBatisXmlMappers() { + return dependentEngineMyBatisXmlMappers; + } + + public AbstractEngineConfiguration setCustomMybatisInterceptors(List customMybatisInterceptors) { + this.customMybatisInterceptors = customMybatisInterceptors; + return this; + } + + public List getCustomMybatisInterceptors() { + return customMybatisInterceptors; + } + + public AbstractEngineConfiguration setDependentEngineMyBatisXmlMappers(Set dependentEngineMyBatisXmlMappers) { + this.dependentEngineMyBatisXmlMappers = dependentEngineMyBatisXmlMappers; + return this; + } + + public List getDependentEngineMybatisTypeAliasConfigs() { + return dependentEngineMybatisTypeAliasConfigs; + } + + public AbstractEngineConfiguration setDependentEngineMybatisTypeAliasConfigs(List dependentEngineMybatisTypeAliasConfigs) { + this.dependentEngineMybatisTypeAliasConfigs = dependentEngineMybatisTypeAliasConfigs; + return this; + } + + public List getDependentEngineMybatisTypeHandlerConfigs() { + return dependentEngineMybatisTypeHandlerConfigs; + } + + public AbstractEngineConfiguration setDependentEngineMybatisTypeHandlerConfigs(List dependentEngineMybatisTypeHandlerConfigs) { + this.dependentEngineMybatisTypeHandlerConfigs = dependentEngineMybatisTypeHandlerConfigs; + return this; + } + + public List getCustomSessionFactories() { + return customSessionFactories; + } + + public AbstractEngineConfiguration addCustomSessionFactory(SessionFactory sessionFactory) { + if (customSessionFactories == null) { + customSessionFactories = new ArrayList<>(); + } + customSessionFactories.add(sessionFactory); + return this; + } + + public AbstractEngineConfiguration setCustomSessionFactories(List customSessionFactories) { + this.customSessionFactories = customSessionFactories; + return this; + } + + public boolean isUsingRelationalDatabase() { + return usingRelationalDatabase; + } + + public AbstractEngineConfiguration setUsingRelationalDatabase(boolean usingRelationalDatabase) { + this.usingRelationalDatabase = usingRelationalDatabase; + return this; + } + + public boolean isUsingSchemaMgmt() { + return usingSchemaMgmt; + } + + public AbstractEngineConfiguration setUsingSchemaMgmt(boolean usingSchema) { + this.usingSchemaMgmt = usingSchema; + return this; + } + + public String getDatabaseTablePrefix() { + return databaseTablePrefix; + } + + public AbstractEngineConfiguration setDatabaseTablePrefix(String databaseTablePrefix) { + this.databaseTablePrefix = databaseTablePrefix; + return this; + } + + public String getDatabaseWildcardEscapeCharacter() { + return databaseWildcardEscapeCharacter; + } + + public AbstractEngineConfiguration setDatabaseWildcardEscapeCharacter(String databaseWildcardEscapeCharacter) { + this.databaseWildcardEscapeCharacter = databaseWildcardEscapeCharacter; + return this; + } + + public String getDatabaseCatalog() { + return databaseCatalog; + } + + public AbstractEngineConfiguration setDatabaseCatalog(String databaseCatalog) { + this.databaseCatalog = databaseCatalog; + return this; + } + + public String getDatabaseSchema() { + return databaseSchema; + } + + public AbstractEngineConfiguration setDatabaseSchema(String databaseSchema) { + this.databaseSchema = databaseSchema; + return this; + } + + public boolean isTablePrefixIsSchema() { + return tablePrefixIsSchema; + } + + public AbstractEngineConfiguration setTablePrefixIsSchema(boolean tablePrefixIsSchema) { + this.tablePrefixIsSchema = tablePrefixIsSchema; + return this; + } + + public boolean isAlwaysLookupLatestDefinitionVersion() { + return alwaysLookupLatestDefinitionVersion; + } + + public AbstractEngineConfiguration setAlwaysLookupLatestDefinitionVersion(boolean alwaysLookupLatestDefinitionVersion) { + this.alwaysLookupLatestDefinitionVersion = alwaysLookupLatestDefinitionVersion; + return this; + } + + public boolean isFallbackToDefaultTenant() { + return fallbackToDefaultTenant; + } + + public AbstractEngineConfiguration setFallbackToDefaultTenant(boolean fallbackToDefaultTenant) { + this.fallbackToDefaultTenant = fallbackToDefaultTenant; + return this; + } + + public AbstractEngineConfiguration setDefaultTenantValue(String defaultTenantValue) { + this.defaultTenantProvider = (tenantId, scope, scopeKey) -> defaultTenantValue; + return this; + } + + public DefaultTenantProvider getDefaultTenantProvider() { + return defaultTenantProvider; + } + + public AbstractEngineConfiguration setDefaultTenantProvider(DefaultTenantProvider defaultTenantProvider) { + this.defaultTenantProvider = defaultTenantProvider; + return this; + } + + public boolean isEnableLogSqlExecutionTime() { + return enableLogSqlExecutionTime; + } + + public void setEnableLogSqlExecutionTime(boolean enableLogSqlExecutionTime) { + this.enableLogSqlExecutionTime = enableLogSqlExecutionTime; + } + + public Map, SessionFactory> getSessionFactories() { + return sessionFactories; + } + + public AbstractEngineConfiguration setSessionFactories(Map, SessionFactory> sessionFactories) { + this.sessionFactories = sessionFactories; + return this; + } + + public String getDatabaseSchemaUpdate() { + return databaseSchemaUpdate; + } + + public AbstractEngineConfiguration setDatabaseSchemaUpdate(String databaseSchemaUpdate) { + this.databaseSchemaUpdate = databaseSchemaUpdate; + return this; + } + + public boolean isUseLockForDatabaseSchemaUpdate() { + return useLockForDatabaseSchemaUpdate; + } + + public AbstractEngineConfiguration setUseLockForDatabaseSchemaUpdate(boolean useLockForDatabaseSchemaUpdate) { + this.useLockForDatabaseSchemaUpdate = useLockForDatabaseSchemaUpdate; + return this; + } + + public boolean isEnableEventDispatcher() { + return enableEventDispatcher; + } + + public AbstractEngineConfiguration setEnableEventDispatcher(boolean enableEventDispatcher) { + this.enableEventDispatcher = enableEventDispatcher; + return this; + } + + public FlowableEventDispatcher getEventDispatcher() { + return eventDispatcher; + } + + public AbstractEngineConfiguration setEventDispatcher(FlowableEventDispatcher eventDispatcher) { + this.eventDispatcher = eventDispatcher; + return this; + } + + public List getEventListeners() { + return eventListeners; + } + + public AbstractEngineConfiguration setEventListeners(List eventListeners) { + this.eventListeners = eventListeners; + return this; + } + + public Map> getTypedEventListeners() { + return typedEventListeners; + } + + public AbstractEngineConfiguration setTypedEventListeners(Map> typedEventListeners) { + this.typedEventListeners = typedEventListeners; + return this; + } + + public List getAdditionalEventDispatchActions() { + return additionalEventDispatchActions; + } + + public AbstractEngineConfiguration setAdditionalEventDispatchActions(List additionalEventDispatchActions) { + this.additionalEventDispatchActions = additionalEventDispatchActions; + return this; + } + + public void initEventDispatcher() { + if (this.eventDispatcher == null) { + this.eventDispatcher = new FlowableEventDispatcherImpl(); + } + + initAdditionalEventDispatchActions(); + + this.eventDispatcher.setEnabled(enableEventDispatcher); + + initEventListeners(); + initTypedEventListeners(); + } + + protected void initEventListeners() { + if (eventListeners != null) { + for (FlowableEventListener listenerToAdd : eventListeners) { + this.eventDispatcher.addEventListener(listenerToAdd); + } + } + } + + protected void initAdditionalEventDispatchActions() { + if (this.additionalEventDispatchActions == null) { + this.additionalEventDispatchActions = new ArrayList<>(); + } + } + + protected void initTypedEventListeners() { + if (typedEventListeners != null) { + for (Map.Entry> listenersToAdd : typedEventListeners.entrySet()) { + // Extract types from the given string + FlowableEngineEventType[] types = FlowableEngineEventType.getTypesFromString(listenersToAdd.getKey()); + + for (FlowableEventListener listenerToAdd : listenersToAdd.getValue()) { + this.eventDispatcher.addEventListener(listenerToAdd, types); + } + } + } + } + + public boolean isLoggingSessionEnabled() { + return loggingListener != null; + } + + public LoggingListener getLoggingListener() { + return loggingListener; + } + + public void setLoggingListener(LoggingListener loggingListener) { + this.loggingListener = loggingListener; + } + + public Clock getClock() { + return clock; + } + + public AbstractEngineConfiguration setClock(Clock clock) { + this.clock = clock; + return this; + } + + public ObjectMapper getObjectMapper() { + return objectMapper; + } + + public AbstractEngineConfiguration setObjectMapper(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + return this; + } + + public int getMaxLengthString() { + if (maxLengthStringVariableType == -1) { + if ("oracle".equalsIgnoreCase(databaseType)) { + return DEFAULT_ORACLE_MAX_LENGTH_STRING; + } else { + return DEFAULT_GENERIC_MAX_LENGTH_STRING; + } + } else { + return maxLengthStringVariableType; + } + } + + public int getMaxLengthStringVariableType() { + return maxLengthStringVariableType; + } + + public AbstractEngineConfiguration setMaxLengthStringVariableType(int maxLengthStringVariableType) { + this.maxLengthStringVariableType = maxLengthStringVariableType; + return this; + } + + public PropertyDataManager getPropertyDataManager() { + return propertyDataManager; + } + + public Duration getLockPollRate() { + return lockPollRate; + } + + public AbstractEngineConfiguration setLockPollRate(Duration lockPollRate) { + this.lockPollRate = lockPollRate; + return this; + } + + public Duration getSchemaLockWaitTime() { + return schemaLockWaitTime; + } + + public void setSchemaLockWaitTime(Duration schemaLockWaitTime) { + this.schemaLockWaitTime = schemaLockWaitTime; + } + + public AbstractEngineConfiguration setPropertyDataManager(PropertyDataManager propertyDataManager) { + this.propertyDataManager = propertyDataManager; + return this; + } + + public PropertyEntityManager getPropertyEntityManager() { + return propertyEntityManager; + } + + public AbstractEngineConfiguration setPropertyEntityManager(PropertyEntityManager propertyEntityManager) { + this.propertyEntityManager = propertyEntityManager; + return this; + } + + public ByteArrayDataManager getByteArrayDataManager() { + return byteArrayDataManager; + } + + public AbstractEngineConfiguration setByteArrayDataManager(ByteArrayDataManager byteArrayDataManager) { + this.byteArrayDataManager = byteArrayDataManager; + return this; + } + + public ByteArrayEntityManager getByteArrayEntityManager() { + return byteArrayEntityManager; + } + + public AbstractEngineConfiguration setByteArrayEntityManager(ByteArrayEntityManager byteArrayEntityManager) { + this.byteArrayEntityManager = byteArrayEntityManager; + return this; + } + + public TableDataManager getTableDataManager() { + return tableDataManager; + } + + public AbstractEngineConfiguration setTableDataManager(TableDataManager tableDataManager) { + this.tableDataManager = tableDataManager; + return this; + } + + public List getDeployers() { + return deployers; + } + + public AbstractEngineConfiguration setDeployers(List deployers) { + this.deployers = deployers; + return this; + } + + public List getCustomPreDeployers() { + return customPreDeployers; + } + + public AbstractEngineConfiguration setCustomPreDeployers(List customPreDeployers) { + this.customPreDeployers = customPreDeployers; + return this; + } + + public List getCustomPostDeployers() { + return customPostDeployers; + } + + public AbstractEngineConfiguration setCustomPostDeployers(List customPostDeployers) { + this.customPostDeployers = customPostDeployers; + return this; + } + + public boolean isEnableConfiguratorServiceLoader() { + return enableConfiguratorServiceLoader; + } + + public AbstractEngineConfiguration setEnableConfiguratorServiceLoader(boolean enableConfiguratorServiceLoader) { + this.enableConfiguratorServiceLoader = enableConfiguratorServiceLoader; + return this; + } + + public List getConfigurators() { + return configurators; + } + + public AbstractEngineConfiguration addConfigurator(EngineConfigurator configurator) { + if (configurators == null) { + configurators = new ArrayList<>(); + } + configurators.add(configurator); + return this; + } + + /** + * @return All {@link EngineConfigurator} instances. Will only contain values after init of the engine. + * Use the {@link #getConfigurators()} or {@link #addConfigurator(EngineConfigurator)} methods otherwise. + */ + public List getAllConfigurators() { + return allConfigurators; + } + + public AbstractEngineConfiguration setConfigurators(List configurators) { + this.configurators = configurators; + return this; + } + + public EngineConfigurator getIdmEngineConfigurator() { + return idmEngineConfigurator; + } + + public AbstractEngineConfiguration setIdmEngineConfigurator(EngineConfigurator idmEngineConfigurator) { + this.idmEngineConfigurator = idmEngineConfigurator; + return this; + } + + public EngineConfigurator getEventRegistryConfigurator() { + return eventRegistryConfigurator; + } + + public AbstractEngineConfiguration setEventRegistryConfigurator(EngineConfigurator eventRegistryConfigurator) { + this.eventRegistryConfigurator = eventRegistryConfigurator; + return this; + } + + public AbstractEngineConfiguration setForceCloseMybatisConnectionPool(boolean forceCloseMybatisConnectionPool) { + this.forceCloseMybatisConnectionPool = forceCloseMybatisConnectionPool; + return this; + } + + public boolean isForceCloseMybatisConnectionPool() { + return forceCloseMybatisConnectionPool; + } +} diff --git a/blade-starter-flowable/src/main/java/org/flowable/common/engine/impl/db/LiquibaseBasedSchemaManager.java b/blade-starter-flowable/src/main/java/org/flowable/common/engine/impl/db/LiquibaseBasedSchemaManager.java new file mode 100644 index 0000000..c9f7021 --- /dev/null +++ b/blade-starter-flowable/src/main/java/org/flowable/common/engine/impl/db/LiquibaseBasedSchemaManager.java @@ -0,0 +1,218 @@ +/* Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flowable.common.engine.impl.db; + +import liquibase.Liquibase; +import liquibase.Scope; +import liquibase.database.Database; +import liquibase.database.DatabaseConnection; +import liquibase.database.DatabaseFactory; +import liquibase.database.jvm.JdbcConnection; +import liquibase.exception.DatabaseException; +import liquibase.resource.ClassLoaderResourceAccessor; +import liquibase.ui.LoggerUIService; +import org.apache.commons.lang3.StringUtils; +import org.flowable.common.engine.api.FlowableException; +import org.flowable.common.engine.impl.AbstractEngineConfiguration; +import org.flowable.common.engine.impl.context.Context; +import org.flowable.common.engine.impl.interceptor.CommandContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.util.ClassUtils; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Level; + +/** + * @author Filip Hrisafov + */ +public abstract class LiquibaseBasedSchemaManager implements SchemaManager { + + private static final String LIQUIBASE_HUB_SERVICE_CLASS_NAME = "liquibase.hub.HubService"; + protected static final Map LIQUIBASE_SCOPE_VALUES = new HashMap<>(); + + static { + if (ClassUtils.isPresent(LIQUIBASE_HUB_SERVICE_CLASS_NAME, null)) { + LIQUIBASE_SCOPE_VALUES.put("liquibase.plugin." + LIQUIBASE_HUB_SERVICE_CLASS_NAME, FlowableLiquibaseHubService.class); + LoggerUIService uiService = new LoggerUIService(); + uiService.setStandardLogLevel(Level.FINE); + LIQUIBASE_SCOPE_VALUES.put(Scope.Attr.ui.name(), uiService); + } + } + + protected final Logger logger = LoggerFactory.getLogger(getClass()); + + protected final String context; + protected final String changeLogFile; + protected final String changeLogPrefix; + + public LiquibaseBasedSchemaManager(String context, String changeLogFile, String changeLogPrefix) { + this.context = context; + this.changeLogFile = changeLogFile; + this.changeLogPrefix = changeLogPrefix; + } + + public void initSchema(String databaseSchemaUpdate) { + try { + if (AbstractEngineConfiguration.DB_SCHEMA_UPDATE_CREATE.equals(databaseSchemaUpdate)) { + runForLiquibase(this::schemaCreate); + } + else if (AbstractEngineConfiguration.DB_SCHEMA_UPDATE_CREATE_DROP.equals(databaseSchemaUpdate)) { + runForLiquibase(this::schemaCreate); + + } else if (AbstractEngineConfiguration.DB_SCHEMA_UPDATE_DROP_CREATE.equals(databaseSchemaUpdate)) { + runForLiquibase(() -> { + schemaDrop(); + schemaCreate(); + }); + + } else if (AbstractEngineConfiguration.DB_SCHEMA_UPDATE_TRUE.equals(databaseSchemaUpdate)) { + runForLiquibase(this::schemaUpdate); + + } else if (AbstractEngineConfiguration.DB_SCHEMA_UPDATE_FALSE.equals(databaseSchemaUpdate)) { + //取消自检查 + //runForLiquibase(this::schemaCheckVersion); + } + } catch (Exception e) { + throw new FlowableException("Error initialising " + context + " data model", e); + } + } + + protected void runForLiquibase(Runnable runnable) throws Exception { + Scope.child(LIQUIBASE_SCOPE_VALUES, runnable::run); + } + + @Override + public void schemaCreate() { + Liquibase liquibase = null; + try { + liquibase = createLiquibaseInstance(getDatabaseConfiguration()); + liquibase.update(context); + } catch (Exception e) { + throw new FlowableException("Error creating " + context + " engine tables", e); + } finally { + closeDatabase(liquibase); + } + } + + @Override + public void schemaDrop() { + Liquibase liquibase = null; + try { + liquibase = createLiquibaseInstance(getDatabaseConfiguration()); + liquibase.dropAll(); + } catch (Exception e) { + throw new FlowableException("Error dropping " + context + " engine tables", e); + } finally { + closeDatabase(liquibase); + } + } + + @Override + public String schemaUpdate() { + Liquibase liquibase = null; + try { + liquibase = createLiquibaseInstance(getDatabaseConfiguration()); + liquibase.update(context); + } catch (Exception e) { + throw new FlowableException("Error updating " + context + " engine tables", e); + } finally { + closeDatabase(liquibase); + } + return null; + } + + @Override + public void schemaCheckVersion() { + Liquibase liquibase = null; + try { + liquibase = createLiquibaseInstance(getDatabaseConfiguration()); + liquibase.validate(); + } catch (Exception e) { + throw new FlowableException("Error validating " + context + " engine schema", e); + } finally { + closeDatabase(liquibase); + } + } + + protected abstract LiquibaseDatabaseConfiguration getDatabaseConfiguration(); + + protected Liquibase createLiquibaseInstance(LiquibaseDatabaseConfiguration databaseConfiguration) throws SQLException { + Connection jdbcConnection = null; + boolean closeConnection = false; + try { + CommandContext commandContext = Context.getCommandContext(); + if (commandContext == null) { + jdbcConnection = databaseConfiguration.getDataSource().getConnection(); + closeConnection = true; + } else { + jdbcConnection = commandContext.getSession(DbSqlSession.class).getSqlSession().getConnection(); + } + + // A commit is needed here, because one of the things that Liquibase does when acquiring its lock + // is doing a rollback, which removes all changes done so far. + // For most databases, this is not a problem as DDL statements are not transactional. + // However for some (e.g. sql server), this would remove all previous statements, which is not wanted, + // hence the extra commit here. + if (!jdbcConnection.getAutoCommit()) { + jdbcConnection.commit(); + } + + DatabaseConnection connection = new JdbcConnection(jdbcConnection); + Database database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(connection); + database.setDatabaseChangeLogTableName(changeLogPrefix + database.getDatabaseChangeLogTableName()); + database.setDatabaseChangeLogLockTableName(changeLogPrefix + database.getDatabaseChangeLogLockTableName()); + + String databaseSchema = databaseConfiguration.getDatabaseSchema(); + if (StringUtils.isNotEmpty(databaseSchema)) { + database.setDefaultSchemaName(databaseSchema); + database.setLiquibaseSchemaName(databaseSchema); + } + + String databaseCatalog = databaseConfiguration.getDatabaseCatalog(); + if (StringUtils.isNotEmpty(databaseCatalog)) { + database.setDefaultCatalogName(databaseCatalog); + database.setLiquibaseCatalogName(databaseCatalog); + } + + return new Liquibase(changeLogFile, new ClassLoaderResourceAccessor(), database); + + } catch (Exception e) { + // We only close the connection if an exception occurred, otherwise the Liquibase instance cannot be used + if (jdbcConnection != null && closeConnection) { + jdbcConnection.close(); + } + throw new FlowableException("Error creating " + context + " liquibase instance", e); + } + } + + protected void closeDatabase(Liquibase liquibase) { + if (liquibase != null) { + Database database = liquibase.getDatabase(); + if (database != null) { + // do not close the shared connection if a command context is currently active + if (Context.getCommandContext() == null) { + try { + database.close(); + } catch (DatabaseException e) { + logger.warn("Error closing database for {}", context, e); + } + } + } + } + } + +} diff --git a/blade-starter-flowable/src/main/java/org/flowable/editor/language/json/converter/CustomBpmnJsonConverterContext.java b/blade-starter-flowable/src/main/java/org/flowable/editor/language/json/converter/CustomBpmnJsonConverterContext.java new file mode 100644 index 0000000..e5b22ef --- /dev/null +++ b/blade-starter-flowable/src/main/java/org/flowable/editor/language/json/converter/CustomBpmnJsonConverterContext.java @@ -0,0 +1,53 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.flowable.editor.language.json.converter; + +import java.util.Map; + +/** + * BpmnJsonCustomConverter + * + * @author Chill + */ +public class CustomBpmnJsonConverterContext extends StandaloneBpmnConverterContext { + private final Map formKeyMap; + private final Map decisionTableKeyMap; + + public CustomBpmnJsonConverterContext(Map formKeyMap, Map decisionTableKeyMap) { + this.formKeyMap = formKeyMap; + this.decisionTableKeyMap = decisionTableKeyMap; + } + + @Override + public String getFormModelKeyForFormModelId(String formModelId) { + return formKeyMap.get(formModelId); + } + + @Override + public String getDecisionTableModelKeyForDecisionTableModelId(String decisionTableModelId) { + return decisionTableKeyMap.get(decisionTableModelId); + } +} diff --git a/blade-starter-flowable/src/main/resources/processes/LeaveProcess.bpmn20.xml b/blade-starter-flowable/src/main/resources/processes/LeaveProcess.bpmn20.xml new file mode 100644 index 0000000..4d86c05 --- /dev/null +++ b/blade-starter-flowable/src/main/resources/processes/LeaveProcess.bpmn20.xml @@ -0,0 +1,123 @@ + + + + 请假流程 + + + + + + + + + + + + + + + + + + + + + + + + 3}]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/blade-starter-holidays/README.md b/blade-starter-holidays/README.md new file mode 100644 index 0000000..dec8247 --- /dev/null +++ b/blade-starter-holidays/README.md @@ -0,0 +1,78 @@ +# blade-starter-holidays + +`blade-starter-holidays` 用来判断日期是否工作日,更具 php 版修改。支持2019年起至2024年 中国法定节假日,以国务院发布的公告为准,随时调整及增加;http://www.gov.cn/zfwj/bgtfd.htm 或 http://www.gov.cn/zhengce/xxgkzl.htm + +## 使用 +### maven +```xml + + org.springblade + blade-starter-holidays + ${version} + +``` + +### gradle +```groovy +compile("org.springblade:blade-starter-holidays:${version}") +``` + +### 注入 bean +```java +@Autowired +private HolidaysApi holidaysApi; +``` + +### 接口使用 +```java +/** + * 获取日期类型 + * + * @param localDate LocalDate + * @return DaysType + */ +DaysType getDaysType(LocalDate localDate); + +/** + * 获取日期类型 + * + * @param localDateTime LocalDateTime + * @return DaysType + */ +DaysType getDaysType(LocalDateTime localDateTime); + +/** + * 获取日期类型 + * + * @param date Date + * @return DaysType + */ +DaysType getDaysType(Date date); + +/** + * 判断是否工作日 + * + * @param localDate LocalDate + * @return 是否工作日 + */ +boolean isWeekdays(LocalDate localDate); + +/** + * 判断是否工作日 + * + * @param localDateTime LocalDateTime + * @return 是否工作日 + */ +boolean isWeekdays(LocalDateTime localDateTime); + +/** + * 判断是否工作日 + * + * @param date Date + * @return 是否工作日 + */ +boolean isWeekdays(Date date); +``` + +## 链接 +- holidays_api PHP 版:https://gitee.com/web/holidays_api \ No newline at end of file diff --git a/blade-starter-holidays/pom.xml b/blade-starter-holidays/pom.xml new file mode 100644 index 0000000..b1b0c50 --- /dev/null +++ b/blade-starter-holidays/pom.xml @@ -0,0 +1,36 @@ + + + 4.0.0 + + BladeX-Tool + org.springblade + ${revision} + + + blade-starter-holidays + ${project.artifactId} + ${project.parent.version} + jar + + + + + org.springblade + blade-core-tool + + + org.springframework.cloud + spring-cloud-context + provided + + + + org.springblade + blade-core-auto + provided + + + + diff --git a/blade-starter-holidays/src/main/java/org/springblade/core/holidays/config/HolidaysApiConfiguration.java b/blade-starter-holidays/src/main/java/org/springblade/core/holidays/config/HolidaysApiConfiguration.java new file mode 100644 index 0000000..3bed678 --- /dev/null +++ b/blade-starter-holidays/src/main/java/org/springblade/core/holidays/config/HolidaysApiConfiguration.java @@ -0,0 +1,51 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.holidays.config; + +import org.springblade.core.holidays.core.HolidaysApi; +import org.springblade.core.holidays.impl.HolidaysApiImpl; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.core.io.ResourceLoader; + +/** + * 配置 + * + * @author L.cm + */ +@AutoConfiguration +@EnableConfigurationProperties(HolidaysApiProperties.class) +public class HolidaysApiConfiguration { + + @Bean + public HolidaysApi holidaysApi(ResourceLoader resourceLoader, + HolidaysApiProperties properties) { + return new HolidaysApiImpl(resourceLoader, properties); + } + +} diff --git a/blade-starter-holidays/src/main/java/org/springblade/core/holidays/config/HolidaysApiProperties.java b/blade-starter-holidays/src/main/java/org/springblade/core/holidays/config/HolidaysApiProperties.java new file mode 100644 index 0000000..ed9297d --- /dev/null +++ b/blade-starter-holidays/src/main/java/org/springblade/core/holidays/config/HolidaysApiProperties.java @@ -0,0 +1,67 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.holidays.config; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.cloud.context.config.annotation.RefreshScope; + +import java.util.ArrayList; +import java.util.List; + +/** + * HolidaysApi 配置类 + * + * @author L.cm + */ +@Getter +@Setter +@RefreshScope +@ConfigurationProperties(HolidaysApiProperties.PREFIX) +public class HolidaysApiProperties { + public static final String PREFIX = "mica.holidays"; + + /** + * 自行扩展的 json 文件路径 + */ + private List extData = new ArrayList<>(); + + @Getter + @Setter + public static class ExtData { + /** + * 年份 + */ + private Integer year; + /** + * 数据目录 + */ + private String dataPath; + } + +} diff --git a/blade-starter-holidays/src/main/java/org/springblade/core/holidays/core/DaysType.java b/blade-starter-holidays/src/main/java/org/springblade/core/holidays/core/DaysType.java new file mode 100644 index 0000000..5f25a87 --- /dev/null +++ b/blade-starter-holidays/src/main/java/org/springblade/core/holidays/core/DaysType.java @@ -0,0 +1,77 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.holidays.core; + +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * 日期类型,工作日对应结果为 0, 休息日对应结果为 1, 节假日对应的结果为 2; + * + * @author L.cm + */ +@Getter +@RequiredArgsConstructor +public enum DaysType { + + /** + * 工作日 + */ + WEEKDAYS((byte) 0), + /** + * 休息日 + */ + REST_DAYS((byte) 1), + /** + * 节假日 + */ + HOLIDAYS((byte) 2); + + @JsonValue + private final byte type; + + /** + * 将 type 转换成枚举 + * + * @param type type + * @return DaysType + */ + public static DaysType from(byte type) { + switch (type) { + case 0: + return WEEKDAYS; + case 1: + return REST_DAYS; + case 2: + return HOLIDAYS; + default: + throw new IllegalArgumentException("未知的 DaysType:" + type); + } + } + +} diff --git a/blade-starter-holidays/src/main/java/org/springblade/core/holidays/core/HolidaysApi.java b/blade-starter-holidays/src/main/java/org/springblade/core/holidays/core/HolidaysApi.java new file mode 100644 index 0000000..8aa87eb --- /dev/null +++ b/blade-starter-holidays/src/main/java/org/springblade/core/holidays/core/HolidaysApi.java @@ -0,0 +1,99 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.holidays.core; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Date; + +/** + * 节假日接口 + * + * @author L.cm + */ +public interface HolidaysApi { + + /** + * 获取日期类型 + * + * @param localDate LocalDate + * @return DaysType + */ + DaysType getDaysType(LocalDate localDate); + + /** + * 获取日期类型 + * + * @param localDateTime LocalDateTime + * @return DaysType + */ + default DaysType getDaysType(LocalDateTime localDateTime) { + return getDaysType(localDateTime.toLocalDate()); + } + + /** + * 获取日期类型 + * + * @param date Date + * @return DaysType + */ + default DaysType getDaysType(Date date) { + return getDaysType(date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate()); + } + + /** + * 判断是否工作日 + * + * @param localDate LocalDate + * @return 是否工作日 + */ + default boolean isWeekdays(LocalDate localDate) { + return DaysType.WEEKDAYS.equals(getDaysType(localDate)); + } + + /** + * 判断是否工作日 + * + * @param localDateTime LocalDateTime + * @return 是否工作日 + */ + default boolean isWeekdays(LocalDateTime localDateTime) { + return DaysType.WEEKDAYS.equals(getDaysType(localDateTime)); + } + + /** + * 判断是否工作日 + * + * @param date Date + * @return 是否工作日 + */ + default boolean isWeekdays(Date date) { + return DaysType.WEEKDAYS.equals(getDaysType(date)); + } + +} diff --git a/blade-starter-holidays/src/main/java/org/springblade/core/holidays/impl/HolidaysApiImpl.java b/blade-starter-holidays/src/main/java/org/springblade/core/holidays/impl/HolidaysApiImpl.java new file mode 100644 index 0000000..1a65802 --- /dev/null +++ b/blade-starter-holidays/src/main/java/org/springblade/core/holidays/impl/HolidaysApiImpl.java @@ -0,0 +1,113 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.holidays.impl; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.holidays.config.HolidaysApiProperties; +import org.springblade.core.holidays.core.DaysType; +import org.springblade.core.holidays.core.HolidaysApi; +import org.springblade.core.tool.jackson.JsonUtil; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; + +import java.io.InputStream; +import java.time.LocalDate; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 节假日实现 + * + * @author L.cm + */ +@Slf4j +@RequiredArgsConstructor +public class HolidaysApiImpl implements HolidaysApi, InitializingBean { + /** + * 存储节假日 + */ + private static final Map> YEAR_DATA_MAP = new HashMap<>(); + private final ResourceLoader resourceLoader; + private final HolidaysApiProperties properties; + + @Override + public DaysType getDaysType(LocalDate localDate) { + int year = localDate.getYear(); + Map dataMap = YEAR_DATA_MAP.get(year); + // 对于没有数据的,我们按正常的周六日来判断, + if (dataMap == null) { + log.error("没有对应年:[{}]的数据,请升级或者自行维护数据!", year); + return isWeekDay(localDate); + } + // 日期信息 + int monthValue = localDate.getMonthValue(); + int dayOfMonth = localDate.getDayOfMonth(); + // 月份和日期 + String monthAndDay = String.format("%02d%02d", monthValue, dayOfMonth); + Byte result = dataMap.get(monthAndDay); + if (result != null) { + return DaysType.from(result); + } else { + return isWeekDay(localDate); + } + } + + @Override + public void afterPropertiesSet() throws Exception { + int[] years = new int[]{2019, 2020, 2021, 2022, 2023, 2024}; + for (int year : years) { + Resource resource = resourceLoader.getResource("classpath:data/" + year + "_data.json"); + try (InputStream inputStream = resource.getInputStream()) { + Map dataMap = JsonUtil.readMap(inputStream, Byte.class); + YEAR_DATA_MAP.put(year, dataMap); + } + } + List extDataList = properties.getExtData(); + for (HolidaysApiProperties.ExtData extData : extDataList) { + String dataPath = extData.getDataPath(); + Resource resource = resourceLoader.getResource(dataPath); + try (InputStream inputStream = resource.getInputStream()) { + Map dataMap = JsonUtil.readMap(inputStream, Byte.class); + YEAR_DATA_MAP.put(extData.getYear(), dataMap); + } + } + } + + /** + * 判断是否工作日 + * + * @param localDate LocalDate + * @return DaysType + */ + private static DaysType isWeekDay(LocalDate localDate) { + int week = localDate.getDayOfWeek().getValue(); + return week == 6 || week == 7 ? DaysType.REST_DAYS : DaysType.WEEKDAYS; + } + +} diff --git a/blade-starter-holidays/src/main/resources/data/2019_data.json b/blade-starter-holidays/src/main/resources/data/2019_data.json new file mode 100644 index 0000000..ff40b3b --- /dev/null +++ b/blade-starter-holidays/src/main/resources/data/2019_data.json @@ -0,0 +1,36 @@ +{ + "0101":2, + "0204":1, + "0205":2, + "0206":2, + "0207":2, + "0208":1, + "0209":1, + "0210":1, + "0405":2, + "0406":1, + "0407":1, + "0501":2, + "0502":1, + "0503":1, + "0504":1, + "0607":2, + "0608":1, + "0609":1, + "0913":2, + "0914":1, + "0915":1, + "1001":2, + "1002":2, + "1003":2, + "1004":1, + "1005":1, + "1006":1, + "1007":1, + "0202":0, + "0203":0, + "0428":0, + "0505":0, + "0929":0, + "1012":0 +} \ No newline at end of file diff --git a/blade-starter-holidays/src/main/resources/data/2020_data.json b/blade-starter-holidays/src/main/resources/data/2020_data.json new file mode 100644 index 0000000..b128495 --- /dev/null +++ b/blade-starter-holidays/src/main/resources/data/2020_data.json @@ -0,0 +1,36 @@ +{ + "0101":2, + "0124":1, + "0125":2, + "0126":2, + "0127":2, + "0128":1, + "0129":1, + "0130":1, + "0404":2, + "0405":1, + "0406":1, + "0501":2, + "0502":1, + "0503":1, + "0504":1, + "0505":1, + "0625":2, + "0626":1, + "0627":1, + "1001":2, + "1002":2, + "1003":2, + "1004":2, + "1005":1, + "1006":1, + "1007":1, + "1008":1, + "0119":0, + "0201":0, + "0426":0, + "0509":0, + "0628":0, + "0927":0, + "1010":0 +} \ No newline at end of file diff --git a/blade-starter-holidays/src/main/resources/data/2021_data.json b/blade-starter-holidays/src/main/resources/data/2021_data.json new file mode 100644 index 0000000..aa04420 --- /dev/null +++ b/blade-starter-holidays/src/main/resources/data/2021_data.json @@ -0,0 +1,40 @@ +{ + "0101":2, + "0102":1, + "0103":1, + "0211":1, + "0212":2, + "0213":2, + "0214":2, + "0215":1, + "0216":1, + "0217":1, + "0403":1, + "0404":2, + "0405":1, + "0501":2, + "0502":1, + "0503":1, + "0504":1, + "0505":1, + "0612":1, + "0613":1, + "0614":2, + "0919":1, + "0920":1, + "0921":2, + "1001":2, + "1002":2, + "1003":2, + "1004":1, + "1005":1, + "1006":1, + "1007":1, + "0207":0, + "0220":0, + "0425":0, + "0508":0, + "0918":0, + "0926":0, + "1009":0 +} \ No newline at end of file diff --git a/blade-starter-holidays/src/main/resources/data/2022_data.json b/blade-starter-holidays/src/main/resources/data/2022_data.json new file mode 100644 index 0000000..9260256 --- /dev/null +++ b/blade-starter-holidays/src/main/resources/data/2022_data.json @@ -0,0 +1,41 @@ +{ + "0101":2, + "0102":1, + "0103":1, + "0131":1, + "0201":2, + "0202":2, + "0203":2, + "0204":1, + "0205":1, + "0206":1, + "0403":1, + "0404":1, + "0405":2, + "0430":1, + "0501":2, + "0502":1, + "0503":1, + "0504":1, + "0603":2, + "0604":1, + "0605":1, + "0910":2, + "0911":1, + "0912":1, + "1001":2, + "1002":2, + "1003":2, + "1004":1, + "1005":1, + "1006":1, + "1007":1, + "0129":0, + "0130":0, + "0402":0, + "0424":0, + "0507":0, + "1008":0, + "1009":0, + "1231":1 +} \ No newline at end of file diff --git a/blade-starter-holidays/src/main/resources/data/2023_data.json b/blade-starter-holidays/src/main/resources/data/2023_data.json new file mode 100644 index 0000000..591a845 --- /dev/null +++ b/blade-starter-holidays/src/main/resources/data/2023_data.json @@ -0,0 +1,35 @@ +{ + "0101":2, + "0102":1, + "0121":1, + "0122":2, + "0123":2, + "0124":2, + "0125":1, + "0126":1, + "0127":1, + "0405":2, + "0429":1, + "0430":1, + "0501":2, + "0502":1, + "0503":1, + "0622":2, + "0623":1, + "0624":1, + "0929":2, + "0930":1, + "1001":2, + "1002":2, + "1003":2, + "1004":1, + "1005":1, + "1006":1, + "0128":0, + "0129":0, + "0423":0, + "0506":0, + "0625":0, + "1007":0, + "1008":0 +} \ No newline at end of file diff --git a/blade-starter-holidays/src/main/resources/data/2024_data.json b/blade-starter-holidays/src/main/resources/data/2024_data.json new file mode 100644 index 0000000..3ab672e --- /dev/null +++ b/blade-starter-holidays/src/main/resources/data/2024_data.json @@ -0,0 +1,40 @@ +{ + "0101":2, + "0210":2, + "0211":2, + "0212":2, + "0213":1, + "0214":1, + "0215":1, + "0216":1, + "0217":1, + "0404":2, + "0405":1, + "0406":1, + "0501":2, + "0502":1, + "0503":1, + "0504":1, + "0505":1, + "0608":1, + "0609":1, + "0610":2, + "0915":1, + "0916":1, + "0917":2, + "1001":2, + "1002":2, + "1003":2, + "1004":1, + "1005":1, + "1006":1, + "1007":1, + "0204":0, + "0218":0, + "0407":0, + "0428":0, + "0511":0, + "0914":0, + "0929":0, + "1012":0 +} diff --git a/blade-starter-holidays/src/test/java/org/springblade/core/holidays/test/HolidaysApiTest.java b/blade-starter-holidays/src/test/java/org/springblade/core/holidays/test/HolidaysApiTest.java new file mode 100644 index 0000000..9f81970 --- /dev/null +++ b/blade-starter-holidays/src/test/java/org/springblade/core/holidays/test/HolidaysApiTest.java @@ -0,0 +1,35 @@ +package org.springblade.core.holidays.test; + +import org.springblade.core.holidays.config.HolidaysApiConfiguration; +import org.springblade.core.holidays.config.HolidaysApiProperties; +import org.springblade.core.holidays.core.DaysType; +import org.springblade.core.holidays.core.HolidaysApi; +import org.springblade.core.holidays.impl.HolidaysApiImpl; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; + +import java.time.LocalDate; + +class HolidaysApiTest { + + private HolidaysApi holidaysApi; + + @BeforeEach + public void setup() throws Exception { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + HolidaysApiConfiguration configuration = new HolidaysApiConfiguration(); + holidaysApi = configuration.holidaysApi(context, new HolidaysApiProperties()); + ((HolidaysApiImpl) holidaysApi).afterPropertiesSet(); + } + + @Test + void test() { + DaysType daysType = holidaysApi.getDaysType(LocalDate.of(2023, 1, 1)); + Assertions.assertEquals(DaysType.HOLIDAYS, daysType); + Assertions.assertFalse(holidaysApi.isWeekdays(LocalDate.of(2023, 9, 29))); + Assertions.assertTrue(holidaysApi.isWeekdays(LocalDate.of(2023, 10, 7))); + } + +} diff --git a/blade-starter-http/pom.xml b/blade-starter-http/pom.xml new file mode 100644 index 0000000..1f4ff31 --- /dev/null +++ b/blade-starter-http/pom.xml @@ -0,0 +1,41 @@ + + + 4.0.0 + + BladeX-Tool + org.springblade + ${revision} + + + blade-starter-http + ${project.artifactId} + ${project.parent.version} + jar + + + + org.springblade + blade-core-tool + + + com.squareup.okhttp3 + okhttp + + + com.squareup.okhttp3 + logging-interceptor + + + org.jsoup + jsoup + + + com.google.code.findbugs + jsr305 + provided + + + + diff --git a/blade-starter-http/src/main/java/org/springblade/core/http/AsyncCall.java b/blade-starter-http/src/main/java/org/springblade/core/http/AsyncCall.java new file mode 100644 index 0000000..56c5ca6 --- /dev/null +++ b/blade-starter-http/src/main/java/org/springblade/core/http/AsyncCall.java @@ -0,0 +1,85 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.http; + +import okhttp3.Call; +import okhttp3.Request; + +import java.io.IOException; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +/** + * 异步执行器 + * + * @author L.cm + */ +public class AsyncCall { + private static final Consumer DEFAULT_CONSUMER = (r) -> {}; + private static final BiConsumer DEFAULT_FAIL_CONSUMER = (r, e) -> {}; + private final Call call; + private Consumer successConsumer; + private Consumer responseConsumer; + private BiConsumer failedBiConsumer; + + AsyncCall(Call call) { + this.call = call; + this.successConsumer = DEFAULT_CONSUMER; + this.responseConsumer = DEFAULT_CONSUMER; + this.failedBiConsumer = DEFAULT_FAIL_CONSUMER; + } + + public void onSuccessful(Consumer consumer) { + this.successConsumer = consumer; + this.execute(); + } + + public void onResponse(Consumer consumer) { + this.responseConsumer = consumer; + this.execute(); + } + + public AsyncCall onFailed(BiConsumer biConsumer) { + this.failedBiConsumer = biConsumer; + return this; + } + + private void execute() { + call.enqueue(new AsyncCallback(this)); + } + + void onResponse(HttpResponse httpResponse) { + responseConsumer.accept(httpResponse); + } + + void onSuccessful(HttpResponse httpResponse) { + successConsumer.accept(httpResponse); + } + + void onFailure(Request request, IOException e) { + failedBiConsumer.accept(request, e); + } +} diff --git a/blade-starter-http/src/main/java/org/springblade/core/http/AsyncCallback.java b/blade-starter-http/src/main/java/org/springblade/core/http/AsyncCallback.java new file mode 100644 index 0000000..5400227 --- /dev/null +++ b/blade-starter-http/src/main/java/org/springblade/core/http/AsyncCallback.java @@ -0,0 +1,65 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.http; + +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.Response; + +import javax.annotation.ParametersAreNonnullByDefault; +import java.io.IOException; + +/** + * 异步处理 + * + * @author L.cm + */ +@ParametersAreNonnullByDefault +public class AsyncCallback implements Callback { + private final AsyncCall asyncCall; + + AsyncCallback(AsyncCall asyncCall) { + this.asyncCall = asyncCall; + } + + @Override + public void onFailure(Call call, IOException e) { + asyncCall.onFailure(call.request(), e); + } + + @Override + public void onResponse(Call call, Response response) throws IOException { + try (HttpResponse httpResponse = new HttpResponse(response)) { + asyncCall.onResponse(httpResponse); + if (response.isSuccessful()) { + asyncCall.onSuccessful(httpResponse); + } else { + asyncCall.onFailure(call.request(), new IOException(httpResponse.message())); + } + } + } + +} diff --git a/blade-starter-http/src/main/java/org/springblade/core/http/BaseAuthenticator.java b/blade-starter-http/src/main/java/org/springblade/core/http/BaseAuthenticator.java new file mode 100644 index 0000000..825c96c --- /dev/null +++ b/blade-starter-http/src/main/java/org/springblade/core/http/BaseAuthenticator.java @@ -0,0 +1,51 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.http; + +import lombok.RequiredArgsConstructor; +import okhttp3.*; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +/** + * BaseAuth + * + * @author L.cm + */ +@RequiredArgsConstructor +public class BaseAuthenticator implements Authenticator { + private final String userName; + private final String password; + + @Override + public Request authenticate(Route route, Response response) throws IOException { + String credential = Credentials.basic(userName, password, StandardCharsets.UTF_8); + return response.request().newBuilder() + .header("Authorization", credential) + .build(); + } +} diff --git a/blade-starter-http/src/main/java/org/springblade/core/http/CssQuery.java b/blade-starter-http/src/main/java/org/springblade/core/http/CssQuery.java new file mode 100644 index 0000000..03b8785 --- /dev/null +++ b/blade-starter-http/src/main/java/org/springblade/core/http/CssQuery.java @@ -0,0 +1,87 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.http; + +import java.lang.annotation.*; + +/** + * xml CssQuery + * + * @author L.cm + */ +@Target({ElementType.FIELD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface CssQuery { + + /** + * CssQuery + * + * @return CssQuery + */ + String value(); + + /** + * 读取的 dom attr + * + *

+ * attr:元素对于的 attr 的值 + * html:整个元素的html + * text:元素内文本 + * allText:多个元素的文本值 + *

+ * + * @return attr + */ + String attr() default ""; + + /** + * 正则,用于对 attr value 处理 + * + * @return regex + */ + String regex() default ""; + + /** + * 默认的正则 group + */ + int DEFAULT_REGEX_GROUP = 0; + + /** + * 正则 group,默认为 0 + * + * @return regexGroup + */ + int regexGroup() default DEFAULT_REGEX_GROUP; + + /** + * 嵌套的内部模型:默认 false + * + * @return 是否为内部模型 + */ + boolean inner() default false; +} diff --git a/blade-starter-http/src/main/java/org/springblade/core/http/CssQueryMethodInterceptor.java b/blade-starter-http/src/main/java/org/springblade/core/http/CssQueryMethodInterceptor.java new file mode 100644 index 0000000..a6f894d --- /dev/null +++ b/blade-starter-http/src/main/java/org/springblade/core/http/CssQueryMethodInterceptor.java @@ -0,0 +1,185 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.http; + +import lombok.RequiredArgsConstructor; +import org.jsoup.nodes.Element; +import org.jsoup.nodes.TextNode; +import org.jsoup.select.Elements; +import org.jsoup.select.Selector; +import org.springblade.core.tool.utils.ConvertUtil; +import org.springblade.core.tool.utils.StringPool; +import org.springblade.core.tool.utils.StringUtil; +import org.springframework.cglib.proxy.MethodInterceptor; +import org.springframework.cglib.proxy.MethodProxy; +import org.springframework.core.ResolvableType; +import org.springframework.core.convert.TypeDescriptor; + +import javax.annotation.Nullable; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** + * 代理模型 + * + * @author L.cm + */ +@RequiredArgsConstructor +public class CssQueryMethodInterceptor implements MethodInterceptor { + private final Class clazz; + private final Element element; + + @Nullable + @Override + public Object intercept(Object object, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { + // 只处理 get 方法 is + String name = method.getName(); + if (!StringUtil.startsWithIgnoreCase(name, StringPool.GET)) { + return methodProxy.invokeSuper(object, args); + } + Field field = clazz.getDeclaredField(StringUtil.firstCharToLower(name.substring(3))); + CssQuery cssQuery = field.getAnnotation(CssQuery.class); + // 没有注解,不代理 + if (cssQuery == null) { + return methodProxy.invokeSuper(object, args); + } + Class returnType = method.getReturnType(); + boolean isColl = Collection.class.isAssignableFrom(returnType); + String cssQueryValue = cssQuery.value(); + // 是否为 bean 中 bean + boolean isInner = cssQuery.inner(); + if (isInner) { + return proxyInner(cssQueryValue, method, returnType, isColl); + } + Object proxyValue = proxyValue(cssQueryValue, cssQuery, returnType, isColl); + if (String.class.isAssignableFrom(returnType)) { + return proxyValue; + } + // 用于读取 field 上的注解 + TypeDescriptor typeDescriptor = new TypeDescriptor(field); + return ConvertUtil.convert(proxyValue, typeDescriptor); + } + + @Nullable + private Object proxyValue(String cssQueryValue, CssQuery cssQuery, Class returnType, boolean isColl) { + if (isColl) { + Elements elements = Selector.select(cssQueryValue, element); + Collection valueList = newColl(returnType); + if (elements.isEmpty()) { + return valueList; + } + for (Element select : elements) { + String value = getValue(select, cssQuery); + if (value != null) { + valueList.add(value); + } + } + return valueList; + } + Element select = Selector.selectFirst(cssQueryValue, element); + return getValue(select, cssQuery); + } + + private Object proxyInner(String cssQueryValue, Method method, Class returnType, boolean isColl) { + if (isColl) { + Elements elements = Selector.select(cssQueryValue, element); + Collection valueList = newColl(returnType); + ResolvableType resolvableType = ResolvableType.forMethodReturnType(method); + Class innerType = resolvableType.getGeneric(0).resolve(); + if (innerType == null) { + throw new IllegalArgumentException("Class " + returnType + " 读取泛型失败。"); + } + for (Element select : elements) { + valueList.add(DomMapper.readValue(select, innerType)); + } + return valueList; + } + Element select = Selector.selectFirst(cssQueryValue, element); + return DomMapper.readValue(select, returnType); + } + + @Nullable + private String getValue(@Nullable Element element, CssQuery cssQuery) { + if (element == null) { + return null; + } + // 读取的属性名 + String attrName = cssQuery.attr(); + // 读取的值 + String attrValue; + if (StringUtil.isBlank(attrName)) { + attrValue = element.outerHtml(); + } else if ("html".equalsIgnoreCase(attrName)) { + attrValue = element.html(); + } else if ("text".equalsIgnoreCase(attrName)) { + attrValue = getText(element); + } else if ("allText".equalsIgnoreCase(attrName)) { + attrValue = element.text(); + } else { + attrValue = element.attr(attrName); + } + // 判断是否需要正则处理 + String regex = cssQuery.regex(); + if (StringUtil.isBlank(attrValue) || StringUtil.isBlank(regex)) { + return attrValue; + } + // 处理正则表达式 + return getRegexValue(regex, cssQuery.regexGroup(), attrValue); + } + + @Nullable + private String getRegexValue(String regex, int regexGroup, String value) { + // 处理正则表达式 + Matcher matcher = Pattern.compile(regex, Pattern.DOTALL | Pattern.CASE_INSENSITIVE).matcher(value); + if (!matcher.find()) { + return null; + } + // 正则 group + if (regexGroup > CssQuery.DEFAULT_REGEX_GROUP) { + return matcher.group(regexGroup); + } + return matcher.group(); + } + + private String getText(Element element) { + return element.childNodes().stream() + .filter(node -> node instanceof TextNode) + .map(node -> (TextNode) node) + .map(TextNode::text) + .collect(Collectors.joining()); + } + + private Collection newColl(Class returnType) { + return Set.class.isAssignableFrom(returnType) ? new HashSet<>() : new ArrayList<>(); + } +} diff --git a/blade-starter-http/src/main/java/org/springblade/core/http/DomMapper.java b/blade-starter-http/src/main/java/org/springblade/core/http/DomMapper.java new file mode 100644 index 0000000..ad61c99 --- /dev/null +++ b/blade-starter-http/src/main/java/org/springblade/core/http/DomMapper.java @@ -0,0 +1,169 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.http; + +import org.jsoup.helper.DataUtil; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.parser.Parser; +import org.jsoup.select.Elements; +import org.springblade.core.tool.utils.Exceptions; +import org.springframework.cglib.proxy.Enhancer; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +/** + * 爬虫 xml 转 bean 基于 jsoup + * + * @author L.cm + */ +public class DomMapper { + + /** + * Returns body to jsoup Document. + * + * @return Document + */ + public static Document asDocument(ResponseSpec response) { + return readDocument(response.asString()); + } + + /** + * 将流读取为 jsoup Document + * + * @param inputStream InputStream + * @return Document + */ + public static Document readDocument(InputStream inputStream) { + try { + return DataUtil.load(inputStream, StandardCharsets.UTF_8.name(), ""); + } catch (IOException e) { + throw Exceptions.unchecked(e); + } + } + + /** + * 将 html 字符串读取为 jsoup Document + * + * @param html String + * @return Document + */ + public static Document readDocument(String html) { + return Parser.parse(html, ""); + } + + /** + * 读取 xml 信息为 java Bean + * + * @param inputStream InputStream + * @param clazz bean Class + * @param 泛型 + * @return 对象 + */ + public static T readValue(InputStream inputStream, final Class clazz) { + return readValue(readDocument(inputStream), clazz); + } + + /** + * 读取 xml 信息为 java Bean + * + * @param html html String + * @param clazz bean Class + * @param 泛型 + * @return 对象 + */ + public static T readValue(String html, final Class clazz) { + return readValue(readDocument(html), clazz); + } + + /** + * 读取 xml 信息为 java Bean + * + * @param doc xml element + * @param clazz bean Class + * @param 泛型 + * @return 对象 + */ + @SuppressWarnings("unchecked") + public static T readValue(final Element doc, final Class clazz) { + Enhancer enhancer = new Enhancer(); + enhancer.setSuperclass(clazz); + enhancer.setUseCache(true); + enhancer.setCallback(new CssQueryMethodInterceptor(clazz, doc)); + return (T) enhancer.create(); + } + + /** + * 读取 xml 信息为 java Bean + * + * @param 泛型 + * @param inputStream InputStream + * @param clazz bean Class + * @return 对象 + */ + public static List readList(InputStream inputStream, final Class clazz) { + return readList(readDocument(inputStream), clazz); + } + + /** + * 读取 xml 信息为 java Bean + * + * @param 泛型 + * @param html html String + * @param clazz bean Class + * @return 对象 + */ + public static List readList(String html, final Class clazz) { + return readList(readDocument(html), clazz); + } + + /** + * 读取 xml 信息为 java Bean + * + * @param doc xml element + * @param clazz bean Class + * @param 泛型 + * @return 对象列表 + */ + public static List readList(Element doc, Class clazz) { + CssQuery annotation = clazz.getAnnotation(CssQuery.class); + if (annotation == null) { + throw new IllegalArgumentException("DomMapper readList " + clazz + " mast has annotation @CssQuery."); + } + String cssQueryValue = annotation.value(); + Elements elements = doc.select(cssQueryValue); + List valueList = new ArrayList<>(); + for (Element element : elements) { + valueList.add(readValue(element, clazz)); + } + return valueList; + } + +} diff --git a/blade-starter-http/src/main/java/org/springblade/core/http/Exchange.java b/blade-starter-http/src/main/java/org/springblade/core/http/Exchange.java new file mode 100644 index 0000000..c9e5c5b --- /dev/null +++ b/blade-starter-http/src/main/java/org/springblade/core/http/Exchange.java @@ -0,0 +1,218 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.http; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import lombok.RequiredArgsConstructor; +import okhttp3.Call; +import okhttp3.Request; +import org.springblade.core.tool.utils.Exceptions; + +import javax.annotation.Nullable; +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.BiConsumer; +import java.util.function.Function; + +/** + * Exchange + * + * @author L.cm + */ +@RequiredArgsConstructor +public class Exchange { + private BiConsumer failedBiConsumer = (r, e) -> {}; + private final Call call; + + public Exchange onFailed(BiConsumer failConsumer) { + this.failedBiConsumer = failConsumer; + return this; + } + + public R onResponse(Function func) { + try (HttpResponse response = new HttpResponse(call.execute())) { + return func.apply(response); + } catch (IOException e) { + throw Exceptions.unchecked(e); + } + } + + @Nullable + public R onSuccess(Function func) { + try (HttpResponse response = new HttpResponse(call.execute())) { + return func.apply(response); + } catch (IOException e) { + failedBiConsumer.accept(call.request(), e); + return null; + } + } + + @Nullable + public R onSuccessful(Function func) { + try (HttpResponse response = new HttpResponse(call.execute())) { + if (response.isOk()) { + return func.apply(response); + } else { + failedBiConsumer.accept(call.request(), new IOException(response.toString())); + } + } catch (IOException e) { + failedBiConsumer.accept(call.request(), e); + } + return null; + } + + public Optional onSuccessOpt(Function func) { + return Optional.ofNullable(this.onSuccess(func)); + } + + public Optional onSuccessfulOpt(Function func) { + return Optional.ofNullable(this.onSuccessful(func)); + } + + /** + * Returns body String. + * + * @return body String + */ + public String asString() { + return onResponse(ResponseSpec::asString); + } + + /** + * Returns body to byte arrays. + * + * @return byte arrays + */ + public byte[] asBytes() { + return onResponse(ResponseSpec::asBytes); + } + + /** + * Returns body to JsonNode. + * + * @return JsonNode + */ + public JsonNode asJsonNode() { + return onResponse(ResponseSpec::asJsonNode); + } + + /** + * Returns body to Object. + * + * @param valueType value value type + * @return Object + */ + public T asValue(Class valueType) { + return onResponse(responseSpec -> responseSpec.asValue(valueType)); + } + + /** + * Returns body to Object. + * + * @param typeReference value Type Reference + * @return Object + */ + public T asValue(TypeReference typeReference) { + return onResponse(responseSpec -> responseSpec.asValue(typeReference)); + } + + /** + * Returns body to List. + * + * @param valueType value type + * @return List + */ + public List asList(Class valueType) { + return onResponse(responseSpec -> responseSpec.asList(valueType)); + } + + /** + * Returns body to Map. + * + * @param keyClass key type + * @param valueType value type + * @return Map + */ + public Map asMap(Class keyClass, Class valueType) { + return onResponse(responseSpec -> responseSpec.asMap(keyClass, valueType)); + } + + /** + * Returns body to Map. + * + * @param valueType value 类型 + * @return Map + */ + public Map asMap(Class valueType) { + return onResponse(responseSpec -> responseSpec.asMap(valueType)); + } + + /** + * 将 xml、heml 转成对象 + * + * @param valueType 对象类 + * @param 泛型 + * @return 对象 + */ + public T asDomValue(Class valueType) { + return onResponse(responseSpec -> responseSpec.asDomValue(valueType)); + } + + /** + * 将 xml、heml 转成对象 + * + * @param valueType 对象类 + * @param 泛型 + * @return 对象集合 + */ + public List asDomList(Class valueType) { + return onResponse(responseSpec -> responseSpec.asDomList(valueType)); + } + + /** + * toFile. + * + * @param file File + */ + public File toFile(File file) { + return onResponse(responseSpec -> responseSpec.toFile(file)); + } + + /** + * toFile. + * + * @param path Path + */ + public Path toFile(Path path) { + return onResponse(responseSpec -> responseSpec.toFile(path)); + } + +} diff --git a/blade-starter-http/src/main/java/org/springblade/core/http/FormBuilder.java b/blade-starter-http/src/main/java/org/springblade/core/http/FormBuilder.java new file mode 100644 index 0000000..16ec0ea --- /dev/null +++ b/blade-starter-http/src/main/java/org/springblade/core/http/FormBuilder.java @@ -0,0 +1,77 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.http; + +import okhttp3.FormBody; + +import javax.annotation.Nullable; +import java.util.Map; + +/** + * 表单构造器 + * + * @author L.cm + */ +public class FormBuilder { + private final HttpRequest request; + private final FormBody.Builder formBuilder; + + FormBuilder(HttpRequest request) { + this.request = request; + this.formBuilder = new FormBody.Builder(); + } + + public FormBuilder add(String name, @Nullable Object value) { + this.formBuilder.add(name, HttpRequest.handleValue(value)); + return this; + } + + public FormBuilder addMap(@Nullable Map formMap) { + if (formMap != null && !formMap.isEmpty()) { + formMap.forEach(this::add); + } + return this; + } + + public FormBuilder addEncoded(String name, @Nullable Object encodedValue) { + this.formBuilder.addEncoded(name, HttpRequest.handleValue(encodedValue)); + return this; + } + + public HttpRequest build() { + FormBody formBody = formBuilder.build(); + this.request.form(formBody); + return this.request; + } + + public Exchange execute() { + return this.build().execute(); + } + + public AsyncCall async() { + return this.build().async(); + } +} diff --git a/blade-starter-http/src/main/java/org/springblade/core/http/HttpRequest.java b/blade-starter-http/src/main/java/org/springblade/core/http/HttpRequest.java new file mode 100644 index 0000000..7a09c9b --- /dev/null +++ b/blade-starter-http/src/main/java/org/springblade/core/http/HttpRequest.java @@ -0,0 +1,501 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.http; + +import okhttp3.*; +import okhttp3.internal.Util; +import okhttp3.internal.http.HttpMethod; +import okhttp3.logging.HttpLoggingInterceptor; +import org.springblade.core.tool.jackson.JsonUtil; +import org.springblade.core.tool.ssl.DisableValidationTrustManager; +import org.springblade.core.tool.ssl.TrustAllHostNames; +import org.springblade.core.tool.utils.Exceptions; +import org.springblade.core.tool.utils.Holder; +import org.springblade.core.tool.utils.StringPool; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.net.ssl.*; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.ProxySelector; +import java.net.URI; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; + +/** + * ok http 封装,请求结构体 + * + * @author L.cm + */ +public class HttpRequest { + private static final String DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36"; + private static final MediaType APPLICATION_JSON = MediaType.parse("application/json;charset=UTF-8"); + private static volatile OkHttpClient httpClient = new OkHttpClient(); + @Nullable + private static HttpLoggingInterceptor globalLoggingInterceptor = null; + private final Request.Builder requestBuilder; + private final HttpUrl.Builder uriBuilder; + private final String httpMethod; + private String userAgent; + @Nullable + private RequestBody requestBody; + @Nullable + private Boolean followRedirects; + @Nullable + private Boolean followSslRedirects; + @Nullable + private HttpLoggingInterceptor.Level level; + @Nullable + private CookieJar cookieJar; + @Nullable + private EventListener eventListener; + private final List interceptors = new ArrayList<>(); + @Nullable + private Authenticator authenticator; + @Nullable + private Duration connectTimeout; + @Nullable + private Duration readTimeout; + @Nullable + private Duration writeTimeout; + @Nullable + private Proxy proxy; + @Nullable + private ProxySelector proxySelector; + @Nullable + private Authenticator proxyAuthenticator; + @Nullable + private RetryPolicy retryPolicy; + @Nullable + private Boolean disableSslValidation; + @Nullable + private HostnameVerifier hostnameVerifier; + @Nullable + private SSLSocketFactory sslSocketFactory; + @Nullable + private X509TrustManager trustManager; + + public static HttpRequest get(final String url) { + return new HttpRequest(new Request.Builder(), url, Method.GET); + } + + public static HttpRequest get(final URI uri) { + return get(uri.toString()); + } + + public static HttpRequest post(final String url) { + return new HttpRequest(new Request.Builder(), url, Method.POST); + } + + public static HttpRequest post(final URI uri) { + return post(uri.toString()); + } + + public static HttpRequest patch(final String url) { + return new HttpRequest(new Request.Builder(), url, Method.PATCH); + } + + public static HttpRequest patch(final URI uri) { + return patch(uri.toString()); + } + + public static HttpRequest put(final String url) { + return new HttpRequest(new Request.Builder(), url, Method.PUT); + } + + public static HttpRequest put(final URI uri) { + return put(uri.toString()); + } + + public static HttpRequest delete(final String url) { + return new HttpRequest(new Request.Builder(), url, Method.DELETE); + } + + public static HttpRequest delete(final URI uri) { + return delete(uri.toString()); + } + + public HttpRequest query(String query) { + this.uriBuilder.query(query); + return this; + } + + public HttpRequest queryEncoded(String encodedQuery) { + this.uriBuilder.encodedQuery(encodedQuery); + return this; + } + + public HttpRequest queryMap(@Nullable Map queryMap) { + if (queryMap != null && !queryMap.isEmpty()) { + queryMap.forEach(this::query); + } + return this; + } + + public HttpRequest query(String name, @Nullable Object value) { + this.uriBuilder.addQueryParameter(name, value == null ? null : String.valueOf(value)); + return this; + } + + public HttpRequest queryEncoded(String encodedName, @Nullable Object encodedValue) { + this.uriBuilder.addEncodedQueryParameter(encodedName, encodedValue == null ? null : String.valueOf(encodedValue)); + return this; + } + + HttpRequest form(FormBody formBody) { + this.requestBody = formBody; + return this; + } + + HttpRequest multipartForm(MultipartBody multipartBody) { + this.requestBody = multipartBody; + return this; + } + + public FormBuilder formBuilder() { + return new FormBuilder(this); + } + + public MultipartFormBuilder multipartFormBuilder() { + return new MultipartFormBuilder(this); + } + + public HttpRequest body(RequestBody requestBody) { + this.requestBody = requestBody; + return this; + } + + public HttpRequest bodyString(String body) { + this.requestBody = RequestBody.create(APPLICATION_JSON, body); + return this; + } + + public HttpRequest bodyString(MediaType contentType, String body) { + this.requestBody = RequestBody.create(contentType, body); + return this; + } + + public HttpRequest bodyJson(@Nonnull Object body) { + return bodyString(JsonUtil.toJson(body)); + } + + private HttpRequest(final Request.Builder requestBuilder, String url, String httpMethod) { + HttpUrl httpUrl = HttpUrl.parse(url); + if (httpUrl == null) { + throw new IllegalArgumentException(String.format("Url 不能解析: %s: [%s]。", httpMethod.toLowerCase(), url)); + } + this.requestBuilder = requestBuilder; + this.uriBuilder = httpUrl.newBuilder(); + this.httpMethod = httpMethod; + this.userAgent = DEFAULT_USER_AGENT; + } + + private Call internalCall(final OkHttpClient client) { + OkHttpClient.Builder builder = client.newBuilder(); + if (connectTimeout != null) { + builder.connectTimeout(connectTimeout.toMillis(), TimeUnit.MILLISECONDS); + } + if (readTimeout != null) { + builder.readTimeout(readTimeout.toMillis(), TimeUnit.MILLISECONDS); + } + if (writeTimeout != null) { + builder.writeTimeout(writeTimeout.toMillis(), TimeUnit.MILLISECONDS); + } + if (proxy != null) { + builder.proxy(proxy); + } + if (proxySelector != null) { + builder.proxySelector(proxySelector); + } + if (proxyAuthenticator != null) { + builder.proxyAuthenticator(proxyAuthenticator); + } + if (hostnameVerifier != null) { + builder.hostnameVerifier(hostnameVerifier); + } + if (sslSocketFactory != null && trustManager != null) { + builder.sslSocketFactory(sslSocketFactory, trustManager); + } + if (Boolean.TRUE.equals(disableSslValidation)) { + disableSslValidation(builder); + } + if (authenticator != null) { + builder.authenticator(authenticator); + } + if (!interceptors.isEmpty()) { + builder.interceptors().addAll(interceptors); + } + if (cookieJar != null) { + builder.cookieJar(cookieJar); + } + if (eventListener != null) { + builder.eventListener(eventListener); + } + if (followRedirects != null) { + builder.followRedirects(followRedirects); + } + if (followSslRedirects != null) { + builder.followSslRedirects(followSslRedirects); + } + if (retryPolicy != null) { + builder.addInterceptor(new RetryInterceptor(retryPolicy)); + } + if (level != null && HttpLoggingInterceptor.Level.NONE != level) { + builder.addInterceptor(getLoggingInterceptor(level)); + } else if (globalLoggingInterceptor != null) { + builder.addInterceptor(globalLoggingInterceptor); + } + // 设置 User-Agent + requestBuilder.header("User-Agent", userAgent); + // url + requestBuilder.url(uriBuilder.build()); + String method = httpMethod; + Request request; + if (HttpMethod.requiresRequestBody(method) && requestBody == null) { + request = requestBuilder.method(method, Util.EMPTY_REQUEST).build(); + } else { + request = requestBuilder.method(method, requestBody).build(); + } + return builder.build().newCall(request); + } + + public Exchange execute() { + return new Exchange(internalCall(httpClient)); + } + + public AsyncCall async() { + return new AsyncCall(internalCall(httpClient)); + } + + public HttpRequest baseAuth(String userName, String password) { + this.authenticator = new BaseAuthenticator(userName, password); + return this; + } + + //// HTTP header operations + public HttpRequest addHeader(final Map headers) { + this.requestBuilder.headers(Headers.of(headers)); + return this; + } + + public HttpRequest addHeader(final String... namesAndValues) { + Headers headers = Headers.of(namesAndValues); + this.requestBuilder.headers(headers); + return this; + } + + public HttpRequest addHeader(final String name, final String value) { + this.requestBuilder.addHeader(name, value); + return this; + } + + public HttpRequest setHeader(final String name, final String value) { + this.requestBuilder.header(name, value); + return this; + } + + public HttpRequest removeHeader(final String name) { + this.requestBuilder.removeHeader(name); + return this; + } + + public HttpRequest addCookie(final Cookie cookie) { + this.addHeader("Cookie", cookie.toString()); + return this; + } + + public HttpRequest cacheControl(final CacheControl cacheControl) { + this.requestBuilder.cacheControl(cacheControl); + return this; + } + + public HttpRequest userAgent(final String userAgent) { + this.userAgent = userAgent; + return this; + } + + public HttpRequest followRedirects(boolean followRedirects) { + this.followRedirects = followRedirects; + return this; + } + + public HttpRequest followSslRedirects(boolean followSslRedirects) { + this.followSslRedirects = followSslRedirects; + return this; + } + + private static HttpLoggingInterceptor getLoggingInterceptor(HttpLoggingInterceptor.Level level) { + HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor(Slf4jLogger.INSTANCE); + loggingInterceptor.setLevel(level); + return loggingInterceptor; + } + + public HttpRequest log() { + this.level = HttpLoggingInterceptor.Level.BODY; + return this; + } + + public HttpRequest log(LogLevel logLevel) { + this.level = logLevel.getLevel(); + return this; + } + + public HttpRequest authenticator(Authenticator authenticator) { + this.authenticator = authenticator; + return this; + } + + public HttpRequest interceptor(Interceptor interceptor) { + this.interceptors.add(interceptor); + return this; + } + + public HttpRequest cookieManager(CookieJar cookieJar) { + this.cookieJar = cookieJar; + return this; + } + + public HttpRequest eventListener(EventListener eventListener) { + this.eventListener = eventListener; + return this; + } + + //// HTTP connection parameter operations + public HttpRequest connectTimeout(final Duration timeout) { + this.connectTimeout = timeout; + return this; + } + + public HttpRequest readTimeout(Duration readTimeout) { + this.readTimeout = readTimeout; + return this; + } + + public HttpRequest writeTimeout(Duration writeTimeout) { + this.writeTimeout = writeTimeout; + return this; + } + + public HttpRequest proxy(final InetSocketAddress address) { + this.proxy = new Proxy(Proxy.Type.HTTP, address); + return this; + } + + public HttpRequest proxySelector(final ProxySelector proxySelector) { + this.proxySelector = proxySelector; + return this; + } + + public HttpRequest proxyAuthenticator(final Authenticator proxyAuthenticator) { + this.proxyAuthenticator = proxyAuthenticator; + return this; + } + + public HttpRequest retry() { + this.retryPolicy = RetryPolicy.INSTANCE; + return this; + } + + public HttpRequest retryOn(Predicate respPredicate) { + this.retryPolicy = new RetryPolicy(respPredicate); + return this; + } + + public HttpRequest retry(int maxAttempts, long sleepMillis) { + this.retryPolicy = new RetryPolicy(maxAttempts, sleepMillis); + return this; + } + + public HttpRequest retry(int maxAttempts, long sleepMillis, Predicate respPredicate) { + this.retryPolicy = new RetryPolicy(maxAttempts, sleepMillis); + return this; + } + + /** + * 关闭 ssl 校验 + * + * @return HttpRequest + */ + public HttpRequest disableSslValidation() { + this.disableSslValidation = Boolean.TRUE; + return this; + } + + public HttpRequest hostnameVerifier(HostnameVerifier hostnameVerifier) { + this.hostnameVerifier = hostnameVerifier; + return this; + } + + public HttpRequest sslSocketFactory(SSLSocketFactory sslSocketFactory, X509TrustManager trustManager) { + this.sslSocketFactory = sslSocketFactory; + this.trustManager = trustManager; + return this; + } + + @Override + public String toString() { + return requestBuilder.toString(); + } + + public static void setHttpClient(OkHttpClient httpClient) { + HttpRequest.httpClient = httpClient; + } + + public static void setGlobalLog(LogLevel logLevel) { + HttpRequest.globalLoggingInterceptor = getLoggingInterceptor(logLevel.getLevel()); + } + + static String handleValue(@Nullable Object value) { + if (value == null) { + return StringPool.EMPTY; + } + if (value instanceof String) { + return (String) value; + } + return String.valueOf(value); + } + + private static void disableSslValidation(OkHttpClient.Builder builder) { + try { + X509TrustManager disabledTrustManager = DisableValidationTrustManager.INSTANCE; + TrustManager[] trustManagers = new TrustManager[]{disabledTrustManager}; + SSLContext sslContext = SSLContext.getInstance("SSL"); + sslContext.init(null, trustManagers, Holder.SECURE_RANDOM); + SSLSocketFactory disabledSslSocketFactory = sslContext.getSocketFactory(); + builder.sslSocketFactory(disabledSslSocketFactory, disabledTrustManager); + builder.hostnameVerifier(TrustAllHostNames.INSTANCE); + } catch (NoSuchAlgorithmException | KeyManagementException e) { + throw Exceptions.unchecked(e); + } + } +} diff --git a/blade-starter-http/src/main/java/org/springblade/core/http/HttpResponse.java b/blade-starter-http/src/main/java/org/springblade/core/http/HttpResponse.java new file mode 100644 index 0000000..dafa112 --- /dev/null +++ b/blade-starter-http/src/main/java/org/springblade/core/http/HttpResponse.java @@ -0,0 +1,208 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.http; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import okhttp3.*; +import okhttp3.internal.Util; +import org.springblade.core.tool.jackson.JsonUtil; +import org.springblade.core.tool.utils.Exceptions; + +import javax.annotation.Nullable; +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; + +/** + * ok http 封装,相应结构体 + * + * @author L.cm + */ +public class HttpResponse implements ResponseSpec, Closeable { + private final Request request; + private final Response response; + private final ResponseBody body; + + HttpResponse(final Response response) { + this.request = response.request(); + this.response = response; + this.body = ifNullBodyToEmpty(response.body()); + } + + @Override + public int code() { + return response.code(); + } + + @Override + public String message() { + return response.message(); + } + + @Override + public boolean isOk() { + return response.isSuccessful(); + } + + @Override + public boolean isRedirect() { + return response.isRedirect(); + } + + @Override + public Headers headers() { + return response.headers(); + } + + @Override + public List cookies() { + return Cookie.parseAll(request.url(), this.headers()); + } + + @Override + public Request rawRequest() { + return this.request; + } + + @Override + public Response rawResponse() { + return this.response; + } + + @Override + public ResponseBody rawBody() { + return this.body; + } + + @Override + public String asString() { + try { + return body.string(); + } catch (IOException e) { + throw Exceptions.unchecked(e); + } + } + + @Override + public byte[] asBytes() { + try { + return body.bytes(); + } catch (IOException e) { + throw Exceptions.unchecked(e); + } + } + + @Override + public InputStream asStream() { + return body.byteStream(); + } + + @Override + public JsonNode asJsonNode() { + return JsonUtil.readTree(asBytes()); + } + + @Override + public T asValue(Class valueType) { + return JsonUtil.readValue(asBytes(), valueType); + } + + @Override + public T asValue(TypeReference typeReference) { + return JsonUtil.readValue(asBytes(), typeReference); + } + + @Override + public List asList(Class valueType) { + return JsonUtil.readList(asBytes(), valueType); + } + + @Override + public Map asMap(Class keyClass, Class valueType) { + return JsonUtil.readMap(asBytes(), keyClass, valueType); + } + + @Override + public Map asMap(Class valueType) { + return this.asMap(String.class, valueType); + } + + @Override + public T asDomValue(Class valueType) { + return DomMapper.readValue(this.asStream(), valueType); + } + + @Override + public List asDomList(Class valueType) { + return DomMapper.readList(this.asStream(), valueType); + } + + @Override + public File toFile(File file) { + toFile(file.toPath()); + return file; + } + + @Override + public Path toFile(Path path) { + try { + Files.copy(this.asStream(), path); + return path; + } catch (IOException e) { + throw Exceptions.unchecked(e); + } + } + + @Override + public MediaType contentType() { + return body.contentType(); + } + + @Override + public long contentLength() { + return body.contentLength(); + } + + @Override + public String toString() { + return response.toString(); + } + + private static ResponseBody ifNullBodyToEmpty(@Nullable ResponseBody body) { + return body == null ? Util.EMPTY_RESPONSE : body; + } + + @Override + public void close() throws IOException { + Util.closeQuietly(this.body); + } +} diff --git a/blade-starter-http/src/main/java/org/springblade/core/http/LogLevel.java b/blade-starter-http/src/main/java/org/springblade/core/http/LogLevel.java new file mode 100644 index 0000000..f0241f4 --- /dev/null +++ b/blade-starter-http/src/main/java/org/springblade/core/http/LogLevel.java @@ -0,0 +1,97 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.http; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import okhttp3.logging.HttpLoggingInterceptor; + +/** + * 日志级别 + * + * @author L.cm + */ +@Getter +@AllArgsConstructor +public enum LogLevel { + /** + * No logs. + */ + NONE(HttpLoggingInterceptor.Level.NONE), + /** + * Logs request and response lines. + * + *

Example: + *

{@code
+	 * --> POST /greeting http/1.1 (3-byte body)
+	 *
+	 * <-- 200 OK (22ms, 6-byte body)
+	 * }
+ */ + BASIC(HttpLoggingInterceptor.Level.BASIC), + /** + * Logs request and response lines and their respective headers. + * + *

Example: + *

{@code
+	 * --> POST /greeting http/1.1
+	 * Host: example.com
+	 * Content-Type: plain/text
+	 * Content-Length: 3
+	 * --> END POST
+	 *
+	 * <-- 200 OK (22ms)
+	 * Content-Type: plain/text
+	 * Content-Length: 6
+	 * <-- END HTTP
+	 * }
+ */ + HEADERS(HttpLoggingInterceptor.Level.HEADERS), + /** + * Logs request and response lines and their respective headers and bodies (if present). + * + *

Example: + *

{@code
+	 * --> POST /greeting http/1.1
+	 * Host: example.com
+	 * Content-Type: plain/text
+	 * Content-Length: 3
+	 *
+	 * Hi?
+	 * --> END POST
+	 *
+	 * <-- 200 OK (22ms)
+	 * Content-Type: plain/text
+	 * Content-Length: 6
+	 *
+	 * Hello!
+	 * <-- END HTTP
+	 * }
+ */ + BODY(HttpLoggingInterceptor.Level.BODY); + + private final HttpLoggingInterceptor.Level level; +} diff --git a/blade-starter-http/src/main/java/org/springblade/core/http/Method.java b/blade-starter-http/src/main/java/org/springblade/core/http/Method.java new file mode 100644 index 0000000..f88f4ba --- /dev/null +++ b/blade-starter-http/src/main/java/org/springblade/core/http/Method.java @@ -0,0 +1,40 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.http; + +/** + * http method + * + * @author dream.lu + */ +public interface Method { + String GET = "GET"; + String POST = "POST"; + String PATCH = "PATCH"; + String PUT = "PUT"; + String DELETE = "DELETE"; +} diff --git a/blade-starter-http/src/main/java/org/springblade/core/http/MultipartFormBuilder.java b/blade-starter-http/src/main/java/org/springblade/core/http/MultipartFormBuilder.java new file mode 100644 index 0000000..5de72bb --- /dev/null +++ b/blade-starter-http/src/main/java/org/springblade/core/http/MultipartFormBuilder.java @@ -0,0 +1,106 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.http; + +import okhttp3.Headers; +import okhttp3.MultipartBody; +import okhttp3.RequestBody; + +import javax.annotation.Nullable; +import java.io.File; +import java.util.Map; + +/** + * 表单构造器 + * + * @author L.cm + */ +public class MultipartFormBuilder { + private final HttpRequest request; + private final MultipartBody.Builder formBuilder; + + MultipartFormBuilder(HttpRequest request) { + this.request = request; + this.formBuilder = new MultipartBody.Builder(); + } + + public MultipartFormBuilder add(String name, @Nullable Object value) { + this.formBuilder.addFormDataPart(name, HttpRequest.handleValue(value)); + return this; + } + + public MultipartFormBuilder addMap(@Nullable Map formMap) { + if (formMap != null && !formMap.isEmpty()) { + formMap.forEach(this::add); + } + return this; + } + + public MultipartFormBuilder add(String name, File file) { + String fileName = file.getName(); + return add(name, fileName, file); + } + + public MultipartFormBuilder add(String name, @Nullable String filename, File file) { + RequestBody fileBody = RequestBody.create(null, file); + return add(name, filename, fileBody); + } + + public MultipartFormBuilder add(String name, @Nullable String filename, RequestBody fileBody) { + this.formBuilder.addFormDataPart(name, filename, fileBody); + return this; + } + + public MultipartFormBuilder add(RequestBody body) { + this.formBuilder.addPart(body); + return this; + } + + public MultipartFormBuilder add(@Nullable Headers headers, RequestBody body) { + this.formBuilder.addPart(headers, body); + return this; + } + + public MultipartFormBuilder add(MultipartBody.Part part) { + this.formBuilder.addPart(part); + return this; + } + + public HttpRequest build() { + formBuilder.setType(MultipartBody.FORM); + MultipartBody formBody = formBuilder.build(); + this.request.multipartForm(formBody); + return this.request; + } + + public Exchange execute() { + return this.build().execute(); + } + + public AsyncCall async() { + return this.build().async(); + } +} diff --git a/blade-starter-http/src/main/java/org/springblade/core/http/ResponseSpec.java b/blade-starter-http/src/main/java/org/springblade/core/http/ResponseSpec.java new file mode 100644 index 0000000..9e4156a --- /dev/null +++ b/blade-starter-http/src/main/java/org/springblade/core/http/ResponseSpec.java @@ -0,0 +1,288 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.http; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import okhttp3.*; + +import javax.annotation.Nullable; +import java.io.File; +import java.io.InputStream; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +/** + * 相应接口 + * + * @author L.cm + */ +public interface ResponseSpec { + + /** + * Returns the HTTP code. + * + * @return code + */ + int code(); + + /** + * Returns the HTTP status message. + * + * @return message + */ + String message(); + + /** + * Returns the HTTP isSuccessful. + * + * @return boolean + */ + default boolean isOk() { + return false; + } + + /** + * Returns the is Redirect. + * + * @return is Redirect + */ + boolean isRedirect(); + + /** + * Returns the Headers. + * + * @return Headers + */ + Headers headers(); + + /** + * Headers Consumer. + * + * @param consumer Consumer + * @return Headers + */ + default ResponseSpec headers(Consumer consumer) { + consumer.accept(this.headers()); + return this; + } + + /** + * Returns the Cookies. + * + * @return Cookie List + */ + List cookies(); + + /** + * 读取消费 cookie + * + * @param consumer Consumer + * @return ResponseSpec + */ + default ResponseSpec cookies(Consumer> consumer) { + consumer.accept(this.cookies()); + return this; + } + + /** + * Returns body String. + * + * @return body String + */ + String asString(); + + /** + * Returns body to byte arrays. + * + * @return byte arrays + */ + byte[] asBytes(); + + /** + * Returns body to InputStream. + * + * @return InputStream + */ + InputStream asStream(); + + /** + * Returns body to JsonNode. + * + * @return JsonNode + */ + JsonNode asJsonNode(); + + /** + * Returns body to Object. + * + * @param valueType value value type + * @return Object + */ + @Nullable + T asValue(Class valueType); + + /** + * Returns body to Object. + * + * @param typeReference value Type Reference + * @return Object + */ + @Nullable + T asValue(TypeReference typeReference); + + /** + * Returns body to List. + * + * @param valueType value type + * @return List + */ + List asList(Class valueType); + + /** + * Returns body to Map. + * + * @param keyClass key type + * @param valueType value type + * @return Map + */ + Map asMap(Class keyClass, Class valueType); + + /** + * Returns body to Map. + * + * @param valueType value 类型 + * @return Map + */ + Map asMap(Class valueType); + + /** + * 将 xml、heml 转成对象 + * + * @param valueType 对象类 + * @param 泛型 + * @return 对象 + */ + T asDomValue(Class valueType); + + /** + * 将 xml、heml 转成对象 + * + * @param valueType 对象类 + * @param 泛型 + * @return 对象集合 + */ + List asDomList(Class valueType); + + /** + * toFile. + * + * @param file File + */ + File toFile(File file); + + /** + * toFile. + * + * @param path Path + */ + Path toFile(Path path); + + /** + * Returns contentType. + * + * @return contentType + */ + @Nullable + MediaType contentType(); + + /** + * Returns contentLength. + * + * @return contentLength + */ + long contentLength(); + + /** + * Returns rawRequest. + * + * @return Request + */ + Request rawRequest(); + + /** + * rawRequest Consumer. + * + * @param consumer Consumer + * @return ResponseSpec + */ + @Nullable + default ResponseSpec rawRequest(Consumer consumer) { + consumer.accept(this.rawRequest()); + return this; + } + + /** + * Returns rawResponse. + * + * @return Response + */ + Response rawResponse(); + + /** + * rawResponse Consumer. + * + * @param consumer Consumer + * @return Response + */ + default ResponseSpec rawResponse(Consumer consumer) { + consumer.accept(this.rawResponse()); + return this; + } + + /** + * Returns rawBody. + * + * @return ResponseBody + */ + @Nullable + ResponseBody rawBody(); + + /** + * rawBody Consumer. + * + * @param consumer Consumer + * @return ResponseBody + */ + @Nullable + default ResponseSpec rawBody(Consumer consumer) { + consumer.accept(this.rawBody()); + return this; + } + +} diff --git a/blade-starter-http/src/main/java/org/springblade/core/http/RetryInterceptor.java b/blade-starter-http/src/main/java/org/springblade/core/http/RetryInterceptor.java new file mode 100644 index 0000000..7ce6134 --- /dev/null +++ b/blade-starter-http/src/main/java/org/springblade/core/http/RetryInterceptor.java @@ -0,0 +1,83 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.http; + +import lombok.RequiredArgsConstructor; +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; +import org.springframework.retry.backoff.FixedBackOffPolicy; +import org.springframework.retry.policy.SimpleRetryPolicy; +import org.springframework.retry.support.RetryTemplate; + +import java.io.IOException; +import java.util.function.Predicate; + +/** + * 重试拦截器,应对代理问题 + * + * @author L.cm + */ +@RequiredArgsConstructor +public class RetryInterceptor implements Interceptor { + private final RetryPolicy retryPolicy; + + @Override + public Response intercept(Chain chain) throws IOException { + Request request = chain.request(); + RetryTemplate template = createRetryTemplate(retryPolicy); + return template.execute(context -> { + Response response = chain.proceed(request); + // 结果集校验 + Predicate respPredicate = retryPolicy.getRespPredicate(); + if (respPredicate == null) { + return response; + } + // copy 一份 body + ResponseBody body = response.peekBody(Long.MAX_VALUE); + try (HttpResponse httpResponse = new HttpResponse(response)) { + if (respPredicate.test(httpResponse)) { + throw new IOException("Http Retry ResponsePredicate test Failure."); + } + } + return response.newBuilder().body(body).build(); + }); + } + + private static RetryTemplate createRetryTemplate(RetryPolicy policy) { + RetryTemplate template = new RetryTemplate(); + // 重试策略 + SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(); + retryPolicy.setMaxAttempts(policy.getMaxAttempts()); + // 设置间隔策略 + FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy(); + backOffPolicy.setBackOffPeriod(policy.getSleepMillis()); + template.setRetryPolicy(retryPolicy); + template.setBackOffPolicy(backOffPolicy); + return template; + } +} diff --git a/blade-starter-http/src/main/java/org/springblade/core/http/RetryPolicy.java b/blade-starter-http/src/main/java/org/springblade/core/http/RetryPolicy.java new file mode 100644 index 0000000..e8ffc1e --- /dev/null +++ b/blade-starter-http/src/main/java/org/springblade/core/http/RetryPolicy.java @@ -0,0 +1,67 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.http; + +import lombok.Getter; +import lombok.ToString; +import org.springframework.retry.policy.SimpleRetryPolicy; + +import javax.annotation.Nullable; +import java.util.function.Predicate; + +/** + * 重试策略 + * + * @author dream.lu + */ +@Getter +@ToString +public class RetryPolicy { + public static final RetryPolicy INSTANCE = new RetryPolicy(); + + private final int maxAttempts; + private final long sleepMillis; + @Nullable + private final Predicate respPredicate; + + public RetryPolicy() { + this(null); + } + + public RetryPolicy(int maxAttempts, long sleepMillis) { + this(maxAttempts, sleepMillis, null); + } + + public RetryPolicy(@Nullable Predicate respPredicate) { + this(SimpleRetryPolicy.DEFAULT_MAX_ATTEMPTS, 0L, respPredicate); + } + + public RetryPolicy(int maxAttempts, long sleepMillis, @Nullable Predicate respPredicate) { + this.maxAttempts = maxAttempts; + this.sleepMillis = sleepMillis; + this.respPredicate = respPredicate; + } +} diff --git a/blade-starter-http/src/main/java/org/springblade/core/http/Slf4jLogger.java b/blade-starter-http/src/main/java/org/springblade/core/http/Slf4jLogger.java new file mode 100644 index 0000000..8d33282 --- /dev/null +++ b/blade-starter-http/src/main/java/org/springblade/core/http/Slf4jLogger.java @@ -0,0 +1,45 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.http; + +import lombok.extern.slf4j.Slf4j; +import okhttp3.logging.HttpLoggingInterceptor; + +/** + * OkHttp Slf4j logger + * + * @author L.cm + */ +@Slf4j +public class Slf4jLogger implements HttpLoggingInterceptor.Logger { + + public static final HttpLoggingInterceptor.Logger INSTANCE = new Slf4jLogger(); + + @Override + public void log(String message) { + log.info(message); + } +} diff --git a/blade-starter-http/src/main/java/org/springblade/core/http/util/HttpUtil.java b/blade-starter-http/src/main/java/org/springblade/core/http/util/HttpUtil.java new file mode 100644 index 0000000..a45fb1b --- /dev/null +++ b/blade-starter-http/src/main/java/org/springblade/core/http/util/HttpUtil.java @@ -0,0 +1,158 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.http.util; + +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.http.Exchange; +import org.springblade.core.http.FormBuilder; +import org.springblade.core.http.HttpRequest; + +import java.util.Map; + +/** + * Http请求工具类 + * + * @author Chill + */ +@Slf4j +public class HttpUtil { + + /** + * GET + * + * @param url 请求的url + * @param queries 请求的参数,在浏览器?后面的数据,没有可以传null + * @return String + */ + public static String get(String url, Map queries) { + return get(url, null, queries); + } + + /** + * GET + * + * @param url 请求的url + * @param header 请求头 + * @param queries 请求的参数,在浏览器?后面的数据,没有可以传null + * @return String + */ + public static String get(String url, Map header, Map queries) { + // 添加请求头 + HttpRequest httpRequest = HttpRequest.get(url); + if (header != null && !header.keySet().isEmpty()) { + header.forEach(httpRequest::addHeader); + } + // 添加参数 + httpRequest.queryMap(queries); + return httpRequest.execute().asString(); + } + + /** + * POST + * + * @param url 请求的url + * @param params post form 提交的参数 + * @return String + */ + public static String post(String url, Map params) { + return exchange(url, null, params).asString(); + } + + /** + * POST + * + * @param url 请求的url + * @param header 请求头 + * @param params post form 提交的参数 + * @return String + */ + public static String post(String url, Map header, Map params) { + return exchange(url, header, params).asString(); + } + + /** + * POST请求发送JSON数据 + * + * @param url 请求的url + * @param json 请求的json串 + * @return String + */ + public static String postJson(String url, String json) { + return exchange(url, null, json).asString(); + } + + /** + * POST请求发送JSON数据 + * + * @param url 请求的url + * @param header 请求头 + * @param json 请求的json串 + * @return String + */ + public static String postJson(String url, Map header, String json) { + return exchange(url, header, json).asString(); + } + + public static Exchange exchange(String url, Map header, Map params) { + HttpRequest httpRequest = HttpRequest.post(ensureHttpUrl(url)); + //添加请求头 + if (header != null && !header.keySet().isEmpty()) { + header.forEach(httpRequest::addHeader); + } + FormBuilder formBuilder = httpRequest.formBuilder(); + //添加参数 + if (params != null && !params.keySet().isEmpty()) { + params.forEach(formBuilder::add); + } + return formBuilder.execute(); + } + + public static Exchange exchange(String url, Map header, String content) { + HttpRequest httpRequest = HttpRequest.post(ensureHttpUrl(url)); + //添加请求头 + if (header != null && !header.keySet().isEmpty()) { + header.forEach(httpRequest::addHeader); + } + return httpRequest.bodyString(content).execute(); + } + + /** + * 确保URL具有http或https协议头 + * + * @param url 要处理的URL字符串 + * @return 处理后的URL字符串 + */ + public static String ensureHttpUrl(String url) { + if (url == null || url.isEmpty()) { + return url; + } + if (!url.toLowerCase().startsWith("http://") && !url.toLowerCase().startsWith("https://")) { + return "http://" + url; + } + return url; + } + +} diff --git a/blade-starter-http/src/test/java/org/springblade/core/http/test/BladeProxySelector.java b/blade-starter-http/src/test/java/org/springblade/core/http/test/BladeProxySelector.java new file mode 100644 index 0000000..527d4db --- /dev/null +++ b/blade-starter-http/src/test/java/org/springblade/core/http/test/BladeProxySelector.java @@ -0,0 +1,70 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.http.test; + +import lombok.extern.slf4j.Slf4j; + +import java.io.IOException; +import java.net.*; +import java.util.ArrayList; +import java.util.List; + +/** + * 代理设置 + * + * @author L.cm + */ +@Slf4j +public class BladeProxySelector extends ProxySelector { + + @Override + public List select(URI uri) { + // 注意代理都不可用 + List proxyList = new ArrayList<>(); + proxyList.add(getProxy("127.0.0.1", 8080)); + proxyList.add(getProxy("127.0.0.1", 8081)); + proxyList.add(getProxy("127.0.0.1", 8082)); + proxyList.add(getProxy("127.0.0.1", 3128)); + return proxyList; + } + + @Override + public void connectFailed(URI uri, SocketAddress address, IOException ioe) { + // 注意:经过测试,此处不会触发 + log.error("ConnectFailed uri:{}, address:{}, ioe:{}", uri, address, ioe); + } + + /** + * 构造 Proxy + * + * @param host host + * @param port 端口 + * @return Proxy 对象 + */ + public static Proxy getProxy(String host, int port) { + return new Proxy(Proxy.Type.HTTP, InetSocketAddress.createUnresolved(host, port)); + } +} diff --git a/blade-starter-http/src/test/java/org/springblade/core/http/test/HttpRequestDemo.java b/blade-starter-http/src/test/java/org/springblade/core/http/test/HttpRequestDemo.java new file mode 100644 index 0000000..8ebb0fe --- /dev/null +++ b/blade-starter-http/src/test/java/org/springblade/core/http/test/HttpRequestDemo.java @@ -0,0 +1,139 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.http.test; + +import com.fasterxml.jackson.databind.JsonNode; +import org.springblade.core.http.HttpRequest; +import org.springblade.core.http.LogLevel; +import org.springblade.core.http.ResponseSpec; +import okhttp3.Cookie; +import org.springblade.core.tool.utils.Base64Util; + +import java.net.URI; +import java.time.Duration; +import java.util.Optional; + +/** + * This example of blade http + * + * @author L.cm + */ +public class HttpRequestDemo { + + public void doc() { + // 设定全局日志级别 NONE,BASIC,HEADERS,BODY, 默认:NONE + HttpRequest.setGlobalLog(LogLevel.BODY); + + // 同步请求 url,方法支持 get、post、patch、put、delete + HttpRequest.get("https://www.baidu.com") + .log(LogLevel.BASIC) //设定本次的日志级别,优先于全局 + .addHeader("x-account-id", "blade001") // 添加 header + .addCookie(new Cookie.Builder() // 添加 cookie + .name("sid") + .value("blade_user_001") + .build() + ) + .query("q", "blade") //设置 url 参数,默认进行 url encode + .queryEncoded("name", "encodedValue") + .formBuilder() // 表单构造器,同类 multipartFormBuilder 文件上传表单 + .add("id", 123123) // 表单参数 + .execute()// 发起请求 + .asJsonNode(); + // 结果集转换,注:如果网络异常等会直接抛出异常。 + // 同类的方法有 asString、asBytes + // json 类响应:asJsonNode、asObject、asList、asMap,采用 jackson 处理 + // xml、html响应:asDocument,采用的 jsoup 处理 + // file 文件:toFile + + // 同步 + String html = HttpRequest.post("https://www.baidu.com") + .execute() + .onSuccess(ResponseSpec::asString);// 处理响应,有网络异常等直接返回 null + + // 同步 + String text = HttpRequest.patch("https://www.baidu.com") + .execute() + .onSuccess(ResponseSpec::asString); + // onSuccess http code in [200..300) 处理响应,有网络异常等直接返回 null + + // 发送异步请求 + HttpRequest.delete("https://www.baidu.com") + .async() // 开启异步 + .onFailed((request, e) -> { // 异常时的处理 + e.printStackTrace(); + }) + .onSuccessful(responseSpec -> { // 消费响应成功 http code in [200..300) + // 注意:响应结果流只能读一次 + JsonNode jsonNode = responseSpec.asJsonNode(); + }); + } + + public static void main(String[] args) { + // 设定全局日志级别 + HttpRequest.setGlobalLog(LogLevel.BODY); + + // 同步,异常时 返回 null + String html = HttpRequest.get("https://www.baidu.com") + .connectTimeout(Duration.ofSeconds(1000)) + .query("test", "a") + .query("name", "張三") + .query("x", 1) + .query("abd", Base64Util.encode("123&$#%")) + .queryEncoded("abc", Base64Util.encode("123&$#%")) + .execute() + .onFailed(((request, e) -> { + e.printStackTrace(); + })) + .onSuccess(ResponseSpec::asString); + System.out.println(html); + + // 同步调用,返回 Optional,异常时返回 Optional.empty() + Optional opt = HttpRequest.post(URI.create("https://www.baidu.com")) + .bodyString("Important stuff") + .formBuilder() + .add("a", "b") + .execute() + .onSuccessOpt(ResponseSpec::asString); + + // 同步,成功时消费(处理) response + HttpRequest.post("https://www.baidu.com/some-form") + .addHeader("X-Custom-header", "stuff") + .execute() + .onFailed((request, e) -> { + e.printStackTrace(); + }) + .onSuccessful(ResponseSpec::asString); + + // async,异步执行结果,失败时打印堆栈 + HttpRequest.get("https://www.baidu.com/some-form") + .async() + .onFailed((request, e) -> { + e.printStackTrace(); + }) + .onSuccessful(System.out::println); + } + +} diff --git a/blade-starter-http/src/test/java/org/springblade/core/http/test/HttpRequestProxyTest.java b/blade-starter-http/src/test/java/org/springblade/core/http/test/HttpRequestProxyTest.java new file mode 100644 index 0000000..437bdbc --- /dev/null +++ b/blade-starter-http/src/test/java/org/springblade/core/http/test/HttpRequestProxyTest.java @@ -0,0 +1,42 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.http.test; + +import org.springblade.core.http.HttpRequest; + +public class HttpRequestProxyTest { + + //@Test(expected = IOException.class) + public void proxy() { + // 代理都不可用 + HttpRequest.get("https://www.baidu.com") + .log() + .retry() + .proxySelector(new BladeProxySelector()) + .execute() + .asString(); + } +} diff --git a/blade-starter-http/src/test/java/org/springblade/core/http/test/OsChina.java b/blade-starter-http/src/test/java/org/springblade/core/http/test/OsChina.java new file mode 100644 index 0000000..1a56684 --- /dev/null +++ b/blade-starter-http/src/test/java/org/springblade/core/http/test/OsChina.java @@ -0,0 +1,22 @@ +package org.springblade.core.http.test; + +import lombok.Getter; +import lombok.Setter; +import org.springblade.core.http.CssQuery; + +import java.util.List; + +@Getter +@Setter +public class OsChina { + + @CssQuery(value = "head > title", attr = "text") + private String title; + + @CssQuery(value = "#v_news .page .news", inner = true) + private List vNews; + + @CssQuery(value = ".blog-container .blog-list div", inner = true) + private List vBlogList; + +} diff --git a/blade-starter-http/src/test/java/org/springblade/core/http/test/OsChinaTest.java b/blade-starter-http/src/test/java/org/springblade/core/http/test/OsChinaTest.java new file mode 100644 index 0000000..ee7165f --- /dev/null +++ b/blade-starter-http/src/test/java/org/springblade/core/http/test/OsChinaTest.java @@ -0,0 +1,38 @@ +package org.springblade.core.http.test; + +import org.springblade.core.http.HttpRequest; + +import java.util.List; + +public class OsChinaTest { + + public static void main(String[] args) { + // 同步,异常返回 null + OsChina oschina = HttpRequest.get("https://www.oschina.net") + .execute() + .onSuccess(responseSpec -> responseSpec.asDomValue(OsChina.class)); + if (oschina == null) { + return; + } + System.out.println(oschina.getTitle()); + + System.out.println("热门新闻"); + + List vNews = oschina.getVNews(); + for (VNews vNew : vNews) { + System.out.println("title:\t" + vNew.getTitle()); + System.out.println("href:\t" + vNew.getHref()); + System.out.println("时间:\t" + vNew.getDate()); + } + + System.out.println("热门博客"); + List vBlogList = oschina.getVBlogList(); + for (VBlog vBlog : vBlogList) { + System.out.println("title:\t" + vBlog.getTitle()); + System.out.println("href:\t" + vBlog.getHref()); + System.out.println("阅读数:\t" + vBlog.getRead()); + System.out.println("评价数:\t" + vBlog.getPing()); + System.out.println("点赞数:\t" + vBlog.getZhan()); + } + } +} diff --git a/blade-starter-http/src/test/java/org/springblade/core/http/test/VBlog.java b/blade-starter-http/src/test/java/org/springblade/core/http/test/VBlog.java new file mode 100644 index 0000000..b7aa3f4 --- /dev/null +++ b/blade-starter-http/src/test/java/org/springblade/core/http/test/VBlog.java @@ -0,0 +1,30 @@ +package org.springblade.core.http.test; + +import lombok.Getter; +import lombok.Setter; +import org.springblade.core.http.CssQuery; + +/** + * 热门博客 + */ +@Getter +@Setter +public class VBlog { + + @CssQuery(value = "a", attr = "title") + private String title; + + @CssQuery(value = "a", attr = "href") + private String href; + + //1341阅/9评/4赞 + @CssQuery(value = "span", attr = "text", regex = "^\\d+") + private Integer read; + + @CssQuery(value = "span", attr = "text", regex = "(\\d*).*/(\\d*).*/(\\d*).*", regexGroup = 2) + private Integer ping; + + @CssQuery(value = "span", attr = "text", regex = "(\\d*).*/(\\d*).*/(\\d*).*", regexGroup = 3) + private Integer zhan; + +} diff --git a/blade-starter-http/src/test/java/org/springblade/core/http/test/VNews.java b/blade-starter-http/src/test/java/org/springblade/core/http/test/VNews.java new file mode 100644 index 0000000..536dbe4 --- /dev/null +++ b/blade-starter-http/src/test/java/org/springblade/core/http/test/VNews.java @@ -0,0 +1,24 @@ +package org.springblade.core.http.test; + +import lombok.Getter; +import lombok.Setter; +import org.springblade.core.http.CssQuery; +import org.springframework.format.annotation.DateTimeFormat; + +import java.util.Date; + +@Setter +@Getter +public class VNews { + + @CssQuery(value = "a", attr = "title") + private String title; + + @CssQuery(value = "a", attr = "href") + private String href; + + @CssQuery(value = ".news-date", attr = "text") + @DateTimeFormat(pattern = "MM/dd") + private Date date; + +} diff --git a/blade-starter-jwt/pom.xml b/blade-starter-jwt/pom.xml new file mode 100644 index 0000000..f1fdb9f --- /dev/null +++ b/blade-starter-jwt/pom.xml @@ -0,0 +1,40 @@ + + + 4.0.0 + + BladeX-Tool + org.springblade + ${revision} + + + blade-starter-jwt + ${project.artifactId} + ${project.parent.version} + jar + + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + io.jsonwebtoken + jjwt-impl + + + io.jsonwebtoken + jjwt-jackson + + + + org.springblade + blade-core-auto + provided + + + + diff --git a/blade-starter-jwt/src/main/java/org/springblade/core/jwt/JwtCrypto.java b/blade-starter-jwt/src/main/java/org/springblade/core/jwt/JwtCrypto.java new file mode 100644 index 0000000..e57a1ec --- /dev/null +++ b/blade-starter-jwt/src/main/java/org/springblade/core/jwt/JwtCrypto.java @@ -0,0 +1,236 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.jwt; + +import lombok.SneakyThrows; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import reactor.util.annotation.Nullable; + +import javax.crypto.Cipher; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Base64; +import java.util.Objects; + +/** + * JwtCrypto + * + * @author Chill + */ +public class JwtCrypto { + + public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; + public static final String BLADE_TOKEN_CRYPTO_KEY = "blade.token.crypto-key"; + + + /** + * Base64加密 + * + * @param content 文本内容 + * @param aesTextKey 文本密钥 + * @return {String} + */ + public static String encryptToString(String content, String aesTextKey) { + return encodeToString(encrypt(content, aesTextKey)); + } + + /** + * Base64加密 + * + * @param content 内容 + * @param aesTextKey 文本密钥 + * @return {String} + */ + public static String encryptToString(byte[] content, String aesTextKey) { + return encodeToString(encrypt(content, aesTextKey)); + } + + /** + * 加密 + * + * @param content 文本内容 + * @param aesTextKey 文本密钥 + * @return byte[] + */ + public static byte[] encrypt(String content, String aesTextKey) { + return encrypt(content.getBytes(DEFAULT_CHARSET), aesTextKey); + } + + /** + * 加密 + * + * @param content 文本内容 + * @param charset 编码 + * @param aesTextKey 文本密钥 + * @return byte[] + */ + public static byte[] encrypt(String content, Charset charset, String aesTextKey) { + return encrypt(content.getBytes(charset), aesTextKey); + } + + /** + * 加密 + * + * @param content 内容 + * @param aesTextKey 文本密钥 + * @return byte[] + */ + public static byte[] encrypt(byte[] content, String aesTextKey) { + return encrypt(content, Objects.requireNonNull(aesTextKey).getBytes(DEFAULT_CHARSET)); + } + + /** + * Base64解密 + * + * @param content 文本内容 + * @param aesTextKey 文本密钥 + * @return {String} + */ + @Nullable + public static String decryptToString(@Nullable String content, @Nullable String aesTextKey) { + if (!StringUtils.hasText(content) || !StringUtils.hasText(aesTextKey)) { + return null; + } + byte[] hexBytes = decrypt(decode(content.getBytes(DEFAULT_CHARSET)), aesTextKey); + return new String(hexBytes, DEFAULT_CHARSET); + } + + + /** + * 解密 + * + * @param content 内容 + * @param aesTextKey 文本密钥 + * @return byte[] + */ + public static byte[] decrypt(byte[] content, String aesTextKey) { + return decrypt(content, Objects.requireNonNull(aesTextKey).getBytes(DEFAULT_CHARSET)); + } + + + /** + * 解密 + * + * @param content 内容 + * @param aesKey 密钥 + * @return byte[] + */ + public static byte[] encrypt(byte[] content, byte[] aesKey) { + return aes(Pkcs7Encoder.encode(content), aesKey, Cipher.ENCRYPT_MODE); + } + + /** + * 加密 + * + * @param encrypted 内容 + * @param aesKey 密钥 + * @return byte[] + */ + public static byte[] decrypt(byte[] encrypted, byte[] aesKey) { + return Pkcs7Encoder.decode(aes(encrypted, aesKey, Cipher.DECRYPT_MODE)); + } + + /** + * ase加密 + * + * @param encrypted 内容 + * @param aesKey 密钥 + * @param mode 模式 + * @return byte[] + */ + @SneakyThrows + private static byte[] aes(byte[] encrypted, byte[] aesKey, int mode) { + Assert.isTrue(aesKey.length == 32, "IllegalAesKey, aesKey's length must be 32"); + Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); + SecretKeySpec keySpec = new SecretKeySpec(aesKey, "AES"); + IvParameterSpec iv = new IvParameterSpec(Arrays.copyOfRange(aesKey, 0, 16)); + cipher.init(mode, keySpec, iv); + return cipher.doFinal(encrypted); + } + + /** + * Base64-encode the given byte array to a String. + * + * @param src the original byte array + * @return the encoded byte array as a UTF-8 String + */ + public static String encodeToString(byte[] src) { + if (src.length == 0) { + return ""; + } + return Base64.getEncoder().encodeToString(src); + } + + /** + * Base64-decode the given byte array. + * + * @param src the encoded byte array + * @return the original byte array + */ + public static byte[] decode(byte[] src) { + if (src.length == 0) { + return src; + } + return Base64.getDecoder().decode(src); + } + + /** + * 提供基于PKCS7算法的加解密接口. + */ + private static class Pkcs7Encoder { + private static final int BLOCK_SIZE = 32; + + private static byte[] encode(byte[] src) { + int count = src.length; + // 计算需要填充的位数 + int amountToPad = BLOCK_SIZE - (count % BLOCK_SIZE); + // 获得补位所用的字符 + byte pad = (byte) (amountToPad & 0xFF); + byte[] pads = new byte[amountToPad]; + Arrays.fill(pads, pad); + int length = count + amountToPad; + byte[] dest = new byte[length]; + System.arraycopy(src, 0, dest, 0, count); + System.arraycopy(pads, 0, dest, count, amountToPad); + return dest; + } + + private static byte[] decode(byte[] decrypted) { + int pad = decrypted[decrypted.length - 1]; + if (pad < 1 || pad > BLOCK_SIZE) { + pad = 0; + } + if (pad > 0) { + return Arrays.copyOfRange(decrypted, 0, decrypted.length - pad); + } + return decrypted; + } + } +} diff --git a/blade-starter-jwt/src/main/java/org/springblade/core/jwt/JwtUtil.java b/blade-starter-jwt/src/main/java/org/springblade/core/jwt/JwtUtil.java new file mode 100644 index 0000000..efe478a --- /dev/null +++ b/blade-starter-jwt/src/main/java/org/springblade/core/jwt/JwtUtil.java @@ -0,0 +1,417 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.jwt; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import lombok.Getter; +import org.springblade.core.jwt.enums.SingleLevel; +import org.springblade.core.jwt.props.JwtProperties; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.util.StringUtils; + +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.concurrent.TimeUnit; + +/** + * Jwt工具类 + * + * @author Chill + */ +public class JwtUtil { + + /** + * token基础配置 + */ + public static String BEARER = "bearer"; + public static String CRYPTO = "crypto"; + public static Integer AUTH_LENGTH = 7; + + /** + * token保存至redis的key + */ + private static final String ACCESS_TOKEN_CACHE = "blade:token"; + private static final String REFRESH_TOKEN_CACHE = "blade:refreshToken"; + private static final String TOKEN_KEY = "token:state:"; + + /** + * jwt配置 + */ + @Getter + private static JwtProperties jwtProperties; + + /** + * redis工具 + */ + @Getter + private static RedisTemplate redisTemplate; + + public static void setJwtProperties(JwtProperties properties) { + if (JwtUtil.jwtProperties == null) { + JwtUtil.jwtProperties = properties; + } + } + + public static void setRedisTemplate(RedisTemplate redisTemplate) { + if (JwtUtil.redisTemplate == null) { + JwtUtil.redisTemplate = redisTemplate; + } + } + + /** + * 签名加密 + */ + public static String getBase64Security() { + return Base64.getEncoder().encodeToString(getJwtProperties().getSignKey().getBytes(StandardCharsets.UTF_8)); + } + + /** + * 获取请求传递的token串 + * + * @param auth token + * @return String + */ + public static String getToken(String auth) { + if (isBearer(auth) || isCrypto(auth)) { + return auth.substring(AUTH_LENGTH); + } + return null; + } + + /** + * 判断token类型为bearer + * + * @param auth token + * @return String + */ + public static Boolean isBearer(String auth) { + if ((auth != null) && (auth.length() > AUTH_LENGTH)) { + String headStr = auth.substring(0, 6).toLowerCase(); + return headStr.compareTo(BEARER) == 0; + } + return false; + } + + /** + * 判断token类型为crypto + * + * @param auth token + * @return String + */ + public static Boolean isCrypto(String auth) { + if ((auth != null) && (auth.length() > AUTH_LENGTH)) { + String headStr = auth.substring(0, 6).toLowerCase(); + return headStr.compareTo(CRYPTO) == 0; + } + return false; + } + + /** + * 解析jsonWebToken + * + * @param jsonWebToken token串 + * @return Claims + */ + public static Claims parseJWT(String jsonWebToken) { + try { + SecretKey secretKey = Keys.hmacShaKeyFor(Base64.getDecoder().decode(getBase64Security())); + return Jwts.parser() + .verifyWith(secretKey) + .build() + .parseSignedClaims(jsonWebToken) + .getPayload(); + } catch (Exception ex) { + return null; + } + } + + /** + * 获取保存在redis的accessToken + * + * @param tenantId 租户id + * @param userId 用户id + * @param accessToken accessToken + * @return accessToken + */ + public static String getAccessToken(String tenantId, String userId, String accessToken) { + return getAccessToken(tenantId, null, userId, accessToken); + } + + /** + * 获取保存在redis的accessToken + * + * @param tenantId 租户id + * @param clientId 应用id + * @param userId 用户id + * @param accessToken accessToken + * @return accessToken + */ + public static String getAccessToken(String tenantId, String clientId, String userId, String accessToken) { + return String.valueOf(getRedisTemplate().opsForValue().get(getAccessTokenKey(tenantId, clientId, userId, accessToken))); + } + + + /** + * 添加accessToken至redis + * + * @param tenantId 租户id + * @param userId 用户id + * @param accessToken accessToken + * @param expire 过期时间 + */ + public static void addAccessToken(String tenantId, String userId, String accessToken, int expire) { + addAccessToken(tenantId, null, userId, accessToken, expire); + } + + /** + * 添加accessToken至redis + * + * @param tenantId 租户id + * @param clientId 应用id + * @param userId 用户id + * @param accessToken accessToken + * @param expire 过期时间 + */ + public static void addAccessToken(String tenantId, String clientId, String userId, String accessToken, int expire) { + getRedisTemplate().delete(getAccessTokenKey(tenantId, clientId, userId, accessToken)); + getRedisTemplate().opsForValue().set(getAccessTokenKey(tenantId, clientId, userId, accessToken), accessToken, expire, TimeUnit.SECONDS); + } + + /** + * 删除保存在redis的accessToken + * + * @param tenantId 租户id + * @param userId 用户id + */ + public static void removeAccessToken(String tenantId, String userId) { + removeAccessToken(tenantId, userId, null); + } + + /** + * 删除保存在redis的accessToken + * + * @param tenantId 租户id + * @param userId 用户id + * @param accessToken accessToken + */ + public static void removeAccessToken(String tenantId, String userId, String accessToken) { + removeAccessToken(tenantId, null, userId, accessToken); + } + + /** + * 删除保存在redis的accessToken + * + * @param tenantId 租户id + * @param clientId 应用id + * @param userId 用户id + * @param accessToken accessToken + */ + public static void removeAccessToken(String tenantId, String clientId, String userId, String accessToken) { + getRedisTemplate().delete(getAccessTokenKey(tenantId, clientId, userId, accessToken)); + } + + /** + * 获取保存在redis的refreshToken + * + * @param tenantId 租户id + * @param userId 用户id + * @return refreshToken + */ + public static String getRefreshToken(String tenantId, String userId) { + return getRefreshToken(tenantId, null, userId, null); + } + + /** + * 获取保存在redis的refreshToken + * + * @param tenantId 租户id + * @param userId 用户id + * @param refreshToken refreshToken + * @return refreshToken + */ + public static String getRefreshToken(String tenantId, String userId, String refreshToken) { + return getRefreshToken(tenantId, null, userId, refreshToken); + } + + /** + * 获取保存在redis的refreshToken + * + * @param tenantId 租户id + * @param clientId 应用id + * @param userId 用户id + * @param refreshToken refreshToken + * @return accessToken + */ + public static String getRefreshToken(String tenantId, String clientId, String userId, String refreshToken) { + return String.valueOf(getRedisTemplate().opsForValue().get(getRefreshTokenKey(tenantId, clientId, userId, refreshToken))); + } + + /** + * 添加refreshToken至redis + * + * @param tenantId 租户id + * @param userId 用户id + * @param refreshToken refreshToken + * @param expire 过期时间 + */ + public static void addRefreshToken(String tenantId, String userId, String refreshToken, int expire) { + addRefreshToken(tenantId, null, userId, refreshToken, expire); + } + + /** + * 添加refreshToken至redis + * + * @param tenantId 租户id + * @param clientId 应用id + * @param userId 用户id + * @param refreshToken refreshToken + * @param expire 过期时间 + */ + public static void addRefreshToken(String tenantId, String clientId, String userId, String refreshToken, int expire) { + getRedisTemplate().delete(getRefreshTokenKey(tenantId, clientId, userId, refreshToken)); + getRedisTemplate().opsForValue().set(getRefreshTokenKey(tenantId, clientId, userId, refreshToken), refreshToken, expire, TimeUnit.SECONDS); + } + + /** + * 删除保存在refreshToken的token + * + * @param tenantId 租户id + * @param userId 用户id + */ + public static void removeRefreshToken(String tenantId, String userId) { + removeRefreshToken(tenantId, null, userId, null); + } + + /** + * 删除保存在refreshToken的token + * + * @param tenantId 租户id + * @param clientId 应用id + * @param userId 用户id + */ + public static void removeRefreshToken(String tenantId, String clientId, String userId) { + removeRefreshToken(tenantId, clientId, userId, null); + } + + /** + * 删除保存在refreshToken的token + * + * @param tenantId 租户id + * @param clientId 应用id + * @param userId 用户id + * @param refreshToken refreshToken + */ + public static void removeRefreshToken(String tenantId, String clientId, String userId, String refreshToken) { + getRedisTemplate().delete(getRefreshTokenKey(tenantId, clientId, userId, refreshToken)); + } + + /** + * 获取accessToken索引 + * + * @param tenantId 租户id + * @param userId 用户id + * @param accessToken accessToken + * @return token索引 + */ + public static String getAccessTokenKey(String tenantId, String userId, String accessToken) { + return getAccessTokenKey(tenantId, null, userId, accessToken); + } + + /** + * 获取accessToken索引 + * + * @param tenantId 租户id + * @param clientId 应用id + * @param userId 用户id + * @param accessToken accessToken + * @return token索引 + */ + public static String getAccessTokenKey(String tenantId, String clientId, String userId, String accessToken) { + return getTokenKey(ACCESS_TOKEN_CACHE, tenantId, clientId, userId, accessToken); + } + + /** + * 获取refreshToken索引 + * + * @param tenantId 租户id + * @param userId 用户id + * @return token索引 + */ + public static String getRefreshTokenKey(String tenantId, String userId) { + return getRefreshTokenKey(tenantId, null, userId, null); + } + + /** + * 获取refreshToken索引 + * + * @param tenantId 租户id + * @param clientId 应用id + * @param userId 用户id + * @return token索引 + */ + public static String getRefreshTokenKey(String tenantId, String clientId, String userId) { + return getRefreshTokenKey(tenantId, clientId, userId, null); + } + + /** + * 获取refreshToken索引 + * + * @param tenantId 租户id + * @param clientId 应用id + * @param userId 用户id + * @param refreshToken refreshToken + * @return token索引 + */ + public static String getRefreshTokenKey(String tenantId, String clientId, String userId, String refreshToken) { + return getTokenKey(REFRESH_TOKEN_CACHE, tenantId, clientId, userId, refreshToken); + } + + /** + * 获取通用Token索引 + * + * @param tokenCache 缓存名 + * @param tenantId 租户id + * @param clientId 应用id + * @param userId 用户id + * @param tokenValue tokenValue + * @return token索引 + */ + public static String getTokenKey(String tokenCache, String tenantId, String clientId, String userId, String tokenValue) { + String key = tenantId.concat(":").concat(tokenCache).concat("::").concat(TOKEN_KEY); + if (getJwtProperties().getSingle() || !StringUtils.hasText(tokenValue)) { + if (getJwtProperties().getSingleLevel() == SingleLevel.CLIENT && StringUtils.hasText(clientId)) { + key = key.concat(clientId).concat(":"); + } + return key.concat(userId); + } else { + return key.concat(tokenValue); + } + } + +} diff --git a/blade-starter-jwt/src/main/java/org/springblade/core/jwt/config/JwtConfiguration.java b/blade-starter-jwt/src/main/java/org/springblade/core/jwt/config/JwtConfiguration.java new file mode 100644 index 0000000..9334df0 --- /dev/null +++ b/blade-starter-jwt/src/main/java/org/springblade/core/jwt/config/JwtConfiguration.java @@ -0,0 +1,69 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.jwt.config; + +import lombok.AllArgsConstructor; +import org.springblade.core.jwt.JwtUtil; +import org.springblade.core.jwt.props.JwtProperties; +import org.springblade.core.jwt.serializer.JwtRedisKeySerializer; +import org.springframework.beans.factory.SmartInitializingSingleton; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer; + +/** + * Jwt配置类 + * + * @author Chill + */ +@AllArgsConstructor +@AutoConfiguration(after = JwtRedisConfiguration.class) +@EnableConfigurationProperties({JwtProperties.class}) +public class JwtConfiguration implements SmartInitializingSingleton { + + private final JwtProperties jwtProperties; + private final RedisConnectionFactory redisConnectionFactory; + + @Override + public void afterSingletonsInstantiated() { + // redisTemplate 实例化 + RedisTemplate redisTemplate = new RedisTemplate<>(); + JwtRedisKeySerializer redisKeySerializer = new JwtRedisKeySerializer(); + JdkSerializationRedisSerializer jdkSerializationRedisSerializer = new JdkSerializationRedisSerializer(); + // key 序列化 + redisTemplate.setKeySerializer(redisKeySerializer); + redisTemplate.setHashKeySerializer(redisKeySerializer); + // value 序列化 + redisTemplate.setValueSerializer(jdkSerializationRedisSerializer); + redisTemplate.setHashValueSerializer(jdkSerializationRedisSerializer); + redisTemplate.setConnectionFactory(redisConnectionFactory); + redisTemplate.afterPropertiesSet(); + JwtUtil.setJwtProperties(jwtProperties); + JwtUtil.setRedisTemplate(redisTemplate); + } +} diff --git a/blade-starter-jwt/src/main/java/org/springblade/core/jwt/config/JwtRedisConfiguration.java b/blade-starter-jwt/src/main/java/org/springblade/core/jwt/config/JwtRedisConfiguration.java new file mode 100644 index 0000000..d2a23a5 --- /dev/null +++ b/blade-starter-jwt/src/main/java/org/springblade/core/jwt/config/JwtRedisConfiguration.java @@ -0,0 +1,61 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.jwt.config; + +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; +import org.springframework.core.annotation.Order; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.cache.RedisCacheWriter; +import org.springframework.data.redis.connection.RedisConnectionFactory; + +import java.time.Duration; + +/** + * RedisTemplate 配置 + * + * @author Chill + */ +@Order +@EnableCaching +@AutoConfiguration(after = RedisAutoConfiguration.class) +public class JwtRedisConfiguration { + + @Bean("redisCacheManager") + @ConditionalOnMissingBean(name = "redisCacheManager") + public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) { + RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig() + .entryTtl(Duration.ofHours(1)); + return RedisCacheManager + .builder(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory)) + .cacheDefaults(redisCacheConfiguration).build(); + } + +} diff --git a/blade-starter-jwt/src/main/java/org/springblade/core/jwt/constant/JwtConstant.java b/blade-starter-jwt/src/main/java/org/springblade/core/jwt/constant/JwtConstant.java new file mode 100644 index 0000000..0413413 --- /dev/null +++ b/blade-starter-jwt/src/main/java/org/springblade/core/jwt/constant/JwtConstant.java @@ -0,0 +1,45 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.jwt.constant; + +/** + * Jwt常量 + * + * @author Chill + */ +public interface JwtConstant { + + /** + * 默认key + */ + String DEFAULT_SECRET_KEY = "bladexisapowerfulmicroservicearchitectureupgradedandoptimizedfromacommercialproject"; + + /** + * key安全长度,具体见:https://tools.ietf.org/html/rfc7518#section-3.2 + */ + int SECRET_KEY_LENGTH = 32; + +} diff --git a/blade-starter-jwt/src/main/java/org/springblade/core/jwt/enums/SingleLevel.java b/blade-starter-jwt/src/main/java/org/springblade/core/jwt/enums/SingleLevel.java new file mode 100644 index 0000000..b904539 --- /dev/null +++ b/blade-starter-jwt/src/main/java/org/springblade/core/jwt/enums/SingleLevel.java @@ -0,0 +1,79 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.jwt.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 单人模式平台枚举 + * + * @author Chill + */ +@Getter +@AllArgsConstructor +public enum SingleLevel { + + /** + * 全平台仅可登录一人 + */ + ALL("all", 1), + + /** + * 各应用仅可登录一人 + */ + CLIENT("client", 2), + ; + + /** + * 名称 + */ + final String name; + /** + * 类型 + */ + final int level; + + /** + * 匹配枚举值 + * + * @param name 名称 + * @return SingleLevel + */ + public static SingleLevel of(String name) { + if (name == null) { + return null; + } + SingleLevel[] values = SingleLevel.values(); + for (SingleLevel singleLevel : values) { + if (singleLevel.name.equals(name)) { + return singleLevel; + } + } + return null; + } + +} diff --git a/blade-starter-jwt/src/main/java/org/springblade/core/jwt/props/JwtProperties.java b/blade-starter-jwt/src/main/java/org/springblade/core/jwt/props/JwtProperties.java new file mode 100644 index 0000000..24bfcdb --- /dev/null +++ b/blade-starter-jwt/src/main/java/org/springblade/core/jwt/props/JwtProperties.java @@ -0,0 +1,78 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.jwt.props; + +import io.jsonwebtoken.JwtException; +import lombok.Data; +import org.springblade.core.jwt.constant.JwtConstant; +import org.springblade.core.jwt.enums.SingleLevel; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * JWT配置 + * + * @author Chill + */ +@Data +@ConfigurationProperties("blade.token") +public class JwtProperties { + + /** + * token是否有状态 + */ + private Boolean state = Boolean.FALSE; + + /** + * 是否只可同时在线一人 + */ + private Boolean single = Boolean.FALSE; + + /** + * 单人模式级别(ALL 全部平台只能有一个,CLIENT 不同客户端只能有一个) + */ + private SingleLevel singleLevel = SingleLevel.ALL; + + /** + * token签名 + */ + private String signKey = ""; + + /** + * token密钥 + */ + private String cryptoKey = ""; + + /** + * 获取签名规则 + */ + public String getSignKey() { + if (this.signKey.length() < JwtConstant.SECRET_KEY_LENGTH) { + throw new JwtException("请配置 blade.token.sign-key 的值, 长度32位以上"); + } + return this.signKey; + } + +} diff --git a/blade-starter-jwt/src/main/java/org/springblade/core/jwt/serializer/JwtRedisKeySerializer.java b/blade-starter-jwt/src/main/java/org/springblade/core/jwt/serializer/JwtRedisKeySerializer.java new file mode 100644 index 0000000..ee442c0 --- /dev/null +++ b/blade-starter-jwt/src/main/java/org/springblade/core/jwt/serializer/JwtRedisKeySerializer.java @@ -0,0 +1,83 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.jwt.serializer; + +import org.springframework.cache.interceptor.SimpleKey; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.data.redis.serializer.RedisSerializer; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Objects; + +/** + * 将redis key序列化为字符串 + * + *

+ * spring cache中的简单基本类型直接使用 StringRedisSerializer 会有问题 + *

+ * + * @author L.cm + */ +public class JwtRedisKeySerializer implements RedisSerializer { + private final Charset charset; + private final ConversionService converter; + + public JwtRedisKeySerializer() { + this(StandardCharsets.UTF_8); + } + + public JwtRedisKeySerializer(Charset charset) { + Objects.requireNonNull(charset, "Charset must not be null"); + this.charset = charset; + this.converter = DefaultConversionService.getSharedInstance(); + } + + @Override + public Object deserialize(byte[] bytes) { + // redis keys 会用到反序列化 + if (bytes == null) { + return null; + } + return new String(bytes, charset); + } + + @Override + public byte[] serialize(Object object) { + String key; + if (object instanceof SimpleKey) { + key = ""; + } else if (object instanceof String) { + key = (String) object; + } else { + key = converter.convert(object, String.class); + } + return Objects.requireNonNull(key).getBytes(this.charset); + } + +} diff --git a/blade-starter-liteflow/pom.xml b/blade-starter-liteflow/pom.xml new file mode 100644 index 0000000..8286591 --- /dev/null +++ b/blade-starter-liteflow/pom.xml @@ -0,0 +1,34 @@ + + + 4.0.0 + + BladeX-Tool + org.springblade + ${revision} + + + blade-starter-liteflow + ${project.artifactId} + ${project.parent.version} + jar + + + + com.yomahub + liteflow-spring-boot-starter + + + org.springblade + blade-core-launch + + + + org.springblade + blade-core-auto + provided + + + + diff --git a/blade-starter-liteflow/src/main/java/org/springblade/core/liteflow/config/LiteflowConfiguration.java b/blade-starter-liteflow/src/main/java/org/springblade/core/liteflow/config/LiteflowConfiguration.java new file mode 100644 index 0000000..f7dbaa3 --- /dev/null +++ b/blade-starter-liteflow/src/main/java/org/springblade/core/liteflow/config/LiteflowConfiguration.java @@ -0,0 +1,41 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.liteflow.config; + +import org.springblade.core.launch.props.BladePropertySource; +import org.springframework.boot.autoconfigure.AutoConfiguration; + +/** + * LiteFlow配置类 + * + * @author Chill + */ +@AutoConfiguration +@BladePropertySource(value = "classpath:/blade-liteflow.yml") +public class LiteflowConfiguration { + + +} diff --git a/blade-starter-liteflow/src/main/java/org/springblade/core/liteflow/launch/LiteFlowLauncherServiceImpl.java b/blade-starter-liteflow/src/main/java/org/springblade/core/liteflow/launch/LiteFlowLauncherServiceImpl.java new file mode 100644 index 0000000..71e7978 --- /dev/null +++ b/blade-starter-liteflow/src/main/java/org/springblade/core/liteflow/launch/LiteFlowLauncherServiceImpl.java @@ -0,0 +1,52 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.liteflow.launch; + +import org.springblade.core.auto.service.AutoService; +import org.springblade.core.launch.service.LauncherService; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.core.Ordered; + +import java.util.Properties; + +/** + * LiteFlow启动配置类 + * + * @author Chill + */ +@AutoService(LauncherService.class) +public class LiteFlowLauncherServiceImpl implements LauncherService { + @Override + public void launcher(SpringApplicationBuilder builder, String appName, String profile, boolean isLocalDev) { + Properties props = System.getProperties(); + props.setProperty("liteflow.print-banner", "false"); + } + + @Override + public int getOrder() { + return Ordered.HIGHEST_PRECEDENCE; + } +} diff --git a/blade-starter-liteflow/src/main/resources/blade-liteflow.yml b/blade-starter-liteflow/src/main/resources/blade-liteflow.yml new file mode 100644 index 0000000..d3e381e --- /dev/null +++ b/blade-starter-liteflow/src/main/resources/blade-liteflow.yml @@ -0,0 +1,3 @@ +liteflow: + rule-source: liteflow/*.el.xml + print-execution-log: true diff --git a/blade-starter-loadbalancer/pom.xml b/blade-starter-loadbalancer/pom.xml new file mode 100644 index 0000000..5ebfabe --- /dev/null +++ b/blade-starter-loadbalancer/pom.xml @@ -0,0 +1,77 @@ + + + 4.0.0 + + BladeX-Tool + org.springblade + ${revision} + + + blade-starter-loadbalancer + ${project.artifactId} + ${project.parent.version} + jar + + + + + org.springframework.cloud + spring-cloud-starter-loadbalancer + + + + io.github.openfeign + feign-okhttp + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + com.alibaba.nacos + nacos-client + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + + + com.alibaba.nacos + nacos-client + + + + + com.alibaba.nacos + nacos-client + + + + org.springframework + spring-web + provided + + + + org.springframework + spring-webflux + provided + + + + org.springblade + blade-core-auto + provided + + + + diff --git a/blade-starter-loadbalancer/src/main/java/org/springblade/core/loadbalancer/config/BladeLoadBalancerConfiguration.java b/blade-starter-loadbalancer/src/main/java/org/springblade/core/loadbalancer/config/BladeLoadBalancerConfiguration.java new file mode 100644 index 0000000..e8cd2ec --- /dev/null +++ b/blade-starter-loadbalancer/src/main/java/org/springblade/core/loadbalancer/config/BladeLoadBalancerConfiguration.java @@ -0,0 +1,69 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.loadbalancer.config; + +import org.springblade.core.loadbalancer.props.BladeLoadBalancerProperties; +import org.springblade.core.loadbalancer.rule.GrayscaleLoadBalancer; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClientConfiguration; +import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClientSpecification; +import org.springframework.cloud.loadbalancer.core.ReactorLoadBalancer; +import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier; +import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.core.annotation.Order; +import org.springframework.core.env.Environment; + +/** + * blade 负载均衡策略 + * + * @author Chill + */ +@AutoConfiguration(before = LoadBalancerClientConfiguration.class) +@EnableConfigurationProperties(BladeLoadBalancerProperties.class) +@ConditionalOnProperty(value = BladeLoadBalancerProperties.PROPERTIES_PREFIX + ".enabled", matchIfMissing = true) +@Order(BladeLoadBalancerConfiguration.REACTIVE_SERVICE_INSTANCE_SUPPLIER_ORDER) +public class BladeLoadBalancerConfiguration { + public static final int REACTIVE_SERVICE_INSTANCE_SUPPLIER_ORDER = 193827465; + + @Bean + public ReactorLoadBalancer reactorServiceInstanceLoadBalancer(Environment environment, + LoadBalancerClientFactory loadBalancerClientFactory, + BladeLoadBalancerProperties bladeLoadBalancerProperties) { + String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME); + return new GrayscaleLoadBalancer(loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), bladeLoadBalancerProperties); + } + + @Bean + public LoadBalancerClientSpecification loadBalancerClientSpecification() { + return new LoadBalancerClientSpecification("default.bladeLoadBalancerConfiguration", + new Class[]{BladeLoadBalancerConfiguration.class}); + } + +} diff --git a/blade-starter-loadbalancer/src/main/java/org/springblade/core/loadbalancer/constant/LoadBalancerConstant.java b/blade-starter-loadbalancer/src/main/java/org/springblade/core/loadbalancer/constant/LoadBalancerConstant.java new file mode 100644 index 0000000..61f0774 --- /dev/null +++ b/blade-starter-loadbalancer/src/main/java/org/springblade/core/loadbalancer/constant/LoadBalancerConstant.java @@ -0,0 +1,40 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.loadbalancer.constant; + +/** + * LoadBalancer 常量 + * + * @author Chill + */ +public interface LoadBalancerConstant { + + /** + * 灰度服务的请求头参数 + */ + String VERSION_NAME = "version"; + +} diff --git a/blade-starter-loadbalancer/src/main/java/org/springblade/core/loadbalancer/props/BladeLoadBalancerProperties.java b/blade-starter-loadbalancer/src/main/java/org/springblade/core/loadbalancer/props/BladeLoadBalancerProperties.java new file mode 100644 index 0000000..33bea26 --- /dev/null +++ b/blade-starter-loadbalancer/src/main/java/org/springblade/core/loadbalancer/props/BladeLoadBalancerProperties.java @@ -0,0 +1,61 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.loadbalancer.props; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.cloud.context.config.annotation.RefreshScope; + +import java.util.ArrayList; +import java.util.List; + +/** + * LoadBalancer 配置 + * + * @author Chill + */ +@Getter +@Setter +@RefreshScope +@ConfigurationProperties(BladeLoadBalancerProperties.PROPERTIES_PREFIX) +public class BladeLoadBalancerProperties { + public static final String PROPERTIES_PREFIX = "blade.loadbalancer"; + + /** + * 是否开启自定义负载均衡 + */ + private boolean enabled = true; + /** + * 灰度服务版本 + */ + private String version; + /** + * 优先的ip列表,支持通配符,例如:10.20.0.8*、10.20.0.* + */ + private List priorIpPattern = new ArrayList<>(); + +} diff --git a/blade-starter-loadbalancer/src/main/java/org/springblade/core/loadbalancer/rule/GrayscaleEnvPostProcessor.java b/blade-starter-loadbalancer/src/main/java/org/springblade/core/loadbalancer/rule/GrayscaleEnvPostProcessor.java new file mode 100644 index 0000000..6e57f67 --- /dev/null +++ b/blade-starter-loadbalancer/src/main/java/org/springblade/core/loadbalancer/rule/GrayscaleEnvPostProcessor.java @@ -0,0 +1,65 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.loadbalancer.rule; + +import org.springblade.core.auto.annotation.AutoEnvPostProcessor; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.env.EnvironmentPostProcessor; +import org.springframework.core.Ordered; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.util.StringUtils; + +/** + * 灰度版本 自动处理 + * + * @author Chill + */ +@AutoEnvPostProcessor +public class GrayscaleEnvPostProcessor implements EnvironmentPostProcessor, Ordered { + private static final String GREYSCALE_KEY = "blade.loadbalancer.version"; + private static final String ELK_KEY = "blade.log.elk.destination"; + + private static final String METADATA_KEY = "spring.cloud.nacos.discovery.metadata.version"; + + @Override + public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { + String version = environment.getProperty(GREYSCALE_KEY); + if (StringUtils.hasText(version)) { + environment.getSystemProperties().put(METADATA_KEY, version); + } + + String elk = environment.getProperty(ELK_KEY); + if (StringUtils.hasText(elk)) { + environment.getSystemProperties().put(ELK_KEY, elk); + } + } + + @Override + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE; + } + +} diff --git a/blade-starter-loadbalancer/src/main/java/org/springblade/core/loadbalancer/rule/GrayscaleLoadBalancer.java b/blade-starter-loadbalancer/src/main/java/org/springblade/core/loadbalancer/rule/GrayscaleLoadBalancer.java new file mode 100644 index 0000000..330a17b --- /dev/null +++ b/blade-starter-loadbalancer/src/main/java/org/springblade/core/loadbalancer/rule/GrayscaleLoadBalancer.java @@ -0,0 +1,124 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.loadbalancer.rule; + +import com.alibaba.nacos.common.utils.StringUtils; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.loadbalancer.props.BladeLoadBalancerProperties; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.loadbalancer.*; +import org.springframework.cloud.loadbalancer.core.NoopServiceInstanceListSupplier; +import org.springframework.cloud.loadbalancer.core.ReactorServiceInstanceLoadBalancer; +import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier; +import org.springframework.http.HttpHeaders; +import org.springframework.util.CollectionUtils; +import org.springframework.util.PatternMatchUtils; +import reactor.core.publisher.Mono; + +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; +import java.util.stream.Collectors; + +import static org.springblade.core.loadbalancer.constant.LoadBalancerConstant.VERSION_NAME; + +/** + * LoadBalancer 负载规则 + * + * @author Chill + */ +@Slf4j +@RequiredArgsConstructor +public class GrayscaleLoadBalancer implements ReactorServiceInstanceLoadBalancer { + private final ObjectProvider serviceInstanceListSupplierProvider; + private final BladeLoadBalancerProperties bladeLoadBalancerProperties; + + @Override + public Mono> choose(Request request) { + ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider + .getIfAvailable(NoopServiceInstanceListSupplier::new); + return supplier.get(request).next() + .map(serviceInstances -> getInstanceResponse(serviceInstances, request)); + } + + /** + * 自定义节点规则返回目标节点 + */ + private Response getInstanceResponse(List instances, Request request) { + // 注册中心无可用实例 返回空 + if (CollectionUtils.isEmpty(instances)) { + return new EmptyResponse(); + } + // 指定ip则返回满足ip的服务 + List priorIpPattern = bladeLoadBalancerProperties.getPriorIpPattern(); + if (!priorIpPattern.isEmpty()) { + String[] priorIpPatterns = priorIpPattern.toArray(new String[0]); + List priorIpInstances = instances.stream().filter( + (i -> PatternMatchUtils.simpleMatch(priorIpPatterns, i.getHost())) + ).collect(Collectors.toList()); + if (!priorIpInstances.isEmpty()) { + instances = priorIpInstances; + } + } + + // 获取灰度版本号 + DefaultRequestContext context = (DefaultRequestContext) request.getContext(); + RequestData requestData = (RequestData) context.getClientRequest(); + HttpHeaders headers = requestData.getHeaders(); + String versionName = headers.getFirst(VERSION_NAME); + + // 没有指定灰度版本则返回正式的服务 + if (StringUtils.isBlank(versionName)) { + List noneGrayscaleInstances = instances.stream().filter( + i -> !i.getMetadata().containsKey(VERSION_NAME) + ).collect(Collectors.toList()); + return randomInstance(noneGrayscaleInstances); + } + + // 指定灰度版本则返回标记的服务 + List grayscaleInstances = instances.stream().filter(i -> { + String versionNameInMetadata = i.getMetadata().get(VERSION_NAME); + return StringUtils.equalsIgnoreCase(versionNameInMetadata, versionName); + }).collect(Collectors.toList()); + return randomInstance(grayscaleInstances); + } + + /** + * 采用随机规则返回 + */ + private Response randomInstance(List instances) { + // 若没有可用节点则返回空 + if (instances.isEmpty()) { + return new EmptyResponse(); + } + + // 挑选随机节点返回 + int randomIndex = ThreadLocalRandom.current().nextInt(instances.size()); + ServiceInstance instance = instances.get(randomIndex % instances.size()); + return new DefaultResponse(instance); + } +} diff --git a/blade-starter-log/pom.xml b/blade-starter-log/pom.xml new file mode 100644 index 0000000..f7f7e61 --- /dev/null +++ b/blade-starter-log/pom.xml @@ -0,0 +1,50 @@ + + + 4.0.0 + + BladeX-Tool + org.springblade + ${revision} + + + blade-starter-log + ${project.artifactId} + ${project.parent.version} + jar + + + + + org.springblade + blade-core-cloud + + + + com.baomidou + mybatis-plus + + + + net.logstash.logback + logstash-logback-encoder + + + org.codehaus.janino + janino + + + + hibernate-validator + org.hibernate.validator + + + + org.springblade + blade-core-auto + provided + + + + diff --git a/blade-starter-log/src/main/java/org/springblade/core/log/annotation/ApiLog.java b/blade-starter-log/src/main/java/org/springblade/core/log/annotation/ApiLog.java new file mode 100644 index 0000000..d339484 --- /dev/null +++ b/blade-starter-log/src/main/java/org/springblade/core/log/annotation/ApiLog.java @@ -0,0 +1,47 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ + +package org.springblade.core.log.annotation; + +import java.lang.annotation.*; + +/** + * 操作日志注解 + * + * @author Chill + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface ApiLog { + + /** + * 日志描述 + * + * @return {String} + */ + String value() default "日志记录"; +} diff --git a/blade-starter-log/src/main/java/org/springblade/core/log/aspect/ApiLogAspect.java b/blade-starter-log/src/main/java/org/springblade/core/log/aspect/ApiLogAspect.java new file mode 100644 index 0000000..5a6443d --- /dev/null +++ b/blade-starter-log/src/main/java/org/springblade/core/log/aspect/ApiLogAspect.java @@ -0,0 +1,62 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ + +package org.springblade.core.log.aspect; + +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.springblade.core.log.annotation.ApiLog; +import org.springblade.core.log.publisher.ApiLogPublisher; + +/** + * 操作日志使用spring event异步入库 + * + * @author Chill + */ +@Slf4j +@Aspect +public class ApiLogAspect { + + @Around("@annotation(apiLog)") + public Object around(ProceedingJoinPoint point, ApiLog apiLog) throws Throwable { + //获取类名 + String className = point.getTarget().getClass().getName(); + //获取方法 + String methodName = point.getSignature().getName(); + // 发送异步日志事件 + long beginTime = System.currentTimeMillis(); + //执行方法 + Object result = point.proceed(); + //执行时长(毫秒) + long time = System.currentTimeMillis() - beginTime; + //记录日志 + ApiLogPublisher.publishEvent(methodName, className, apiLog, time); + return result; + } + +} diff --git a/blade-starter-log/src/main/java/org/springblade/core/log/aspect/LogTraceAspect.java b/blade-starter-log/src/main/java/org/springblade/core/log/aspect/LogTraceAspect.java new file mode 100644 index 0000000..7712d5e --- /dev/null +++ b/blade-starter-log/src/main/java/org/springblade/core/log/aspect/LogTraceAspect.java @@ -0,0 +1,55 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.log.aspect; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.springblade.core.log.utils.LogTraceUtil; + +/** + * 为异步方法添加traceId + * + * @author Chill + */ +@Aspect +public class LogTraceAspect { + + @Pointcut("@annotation(org.springframework.scheduling.annotation.Async)") + public void logPointCut() { + } + + @Around("logPointCut()") + public Object around(ProceedingJoinPoint point) throws Throwable { + try { + LogTraceUtil.insert(); + return point.proceed(); + } finally { + LogTraceUtil.remove(); + } + } +} diff --git a/blade-starter-log/src/main/java/org/springblade/core/log/aspect/RequestLogAspect.java b/blade-starter-log/src/main/java/org/springblade/core/log/aspect/RequestLogAspect.java new file mode 100644 index 0000000..a6e4927 --- /dev/null +++ b/blade-starter-log/src/main/java/org/springblade/core/log/aspect/RequestLogAspect.java @@ -0,0 +1,280 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.log.aspect; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.reflect.MethodSignature; +import org.springblade.core.launch.log.BladeLogLevel; +import org.springblade.core.log.props.BladeRequestLogProperties; +import org.springblade.core.tool.jackson.JsonUtil; +import org.springblade.core.tool.utils.ClassUtil; +import org.springblade.core.tool.utils.StringPool; +import org.springblade.core.tool.utils.StringUtil; +import org.springblade.core.tool.utils.WebUtil; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.core.MethodParameter; +import org.springframework.core.io.InputStreamSource; +import org.springframework.util.AntPathMatcher; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.multipart.MultipartFile; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.InputStream; +import java.lang.reflect.Method; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Spring boot 控制器 请求日志,方便代码调试 + * + * @author L.cm + */ +@Slf4j +@Aspect +@AutoConfiguration +@RequiredArgsConstructor +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) +@ConditionalOnProperty(value = BladeLogLevel.REQ_LOG_PROPS_PREFIX + ".enabled", havingValue = "true", matchIfMissing = true) +public class RequestLogAspect { + + private final BladeRequestLogProperties properties; + private final AntPathMatcher antPathMatcher = new AntPathMatcher(); + + /** + * AOP 环切 控制器 R 返回值 + * + * @param point JoinPoint + * @return Object + * @throws Throwable 异常 + */ + @Around( + "execution(!static org.springblade.core.tool.api.R *(..)) && " + + "(@within(org.springframework.stereotype.Controller) || " + + "@within(org.springframework.web.bind.annotation.RestController))" + ) + public Object aroundApi(ProceedingJoinPoint point) throws Throwable { + BladeLogLevel level = properties.getLevel(); + // 不打印日志,直接返回 + if (BladeLogLevel.NONE == level) { + return point.proceed(); + } + HttpServletRequest request = WebUtil.getRequest(); + String requestUrl = Objects.requireNonNull(request).getRequestURI(); + String requestMethod = request.getMethod(); + + // 放行的接口不打印日志 + if (isSkip(requestUrl)) { + return point.proceed(); + } + + // 构建成一条长 日志,避免并发下日志错乱 + StringBuilder beforeReqLog = new StringBuilder(300); + // 日志参数 + List beforeReqArgs = new ArrayList<>(); + beforeReqLog.append("\n\n================ Request Start ================\n"); + // 打印路由 + beforeReqLog.append("===> {}: {}"); + beforeReqArgs.add(requestMethod); + beforeReqArgs.add(requestUrl); + // 打印请求参数 + logIngArgs(point, beforeReqLog, beforeReqArgs); + // 打印请求 headers + logIngHeaders(request, level, beforeReqLog, beforeReqArgs); + beforeReqLog.append("================ Request End ================\n"); + + // 打印执行时间 + long startNs = System.nanoTime(); + log.info(beforeReqLog.toString(), beforeReqArgs.toArray()); + // aop 执行后的日志 + StringBuilder afterReqLog = new StringBuilder(200); + // 日志参数 + List afterReqArgs = new ArrayList<>(); + afterReqLog.append("\n\n=============== Response Start ================\n"); + try { + Object result = point.proceed(); + // 打印返回结构体 + if (BladeLogLevel.BODY.lte(level)) { + afterReqLog.append("===Result=== {}\n"); + afterReqArgs.add(JsonUtil.toJson(result)); + } + return result; + } finally { + long tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs); + afterReqLog.append("<=== {}: {} ({} ms)\n"); + afterReqArgs.add(requestMethod); + afterReqArgs.add(requestUrl); + afterReqArgs.add(tookMs); + afterReqLog.append("=============== Response End ================\n"); + log.info(afterReqLog.toString(), afterReqArgs.toArray()); + } + } + + /** + * 激励请求参数 + * + * @param point ProceedingJoinPoint + * @param beforeReqLog StringBuilder + * @param beforeReqArgs beforeReqArgs + */ + public void logIngArgs(ProceedingJoinPoint point, StringBuilder beforeReqLog, List beforeReqArgs) { + MethodSignature ms = (MethodSignature) point.getSignature(); + Method method = ms.getMethod(); + Object[] args = point.getArgs(); + // 请求参数处理 + final Map paraMap = new HashMap<>(16); + // 一次请求只能有一个 request body + Object requestBodyValue = null; + for (int i = 0; i < args.length; i++) { + // 读取方法参数 + MethodParameter methodParam = ClassUtil.getMethodParameter(method, i); + // PathVariable 参数跳过 + PathVariable pathVariable = methodParam.getParameterAnnotation(PathVariable.class); + if (pathVariable != null) { + continue; + } + RequestBody requestBody = methodParam.getParameterAnnotation(RequestBody.class); + String parameterName = methodParam.getParameterName(); + Object value = args[i]; + // 如果是body的json则是对象 + if (requestBody != null) { + requestBodyValue = value; + continue; + } + // 处理 参数 + if (value instanceof HttpServletRequest) { + paraMap.putAll(((HttpServletRequest) value).getParameterMap()); + continue; + } else if (value instanceof WebRequest) { + paraMap.putAll(((WebRequest) value).getParameterMap()); + continue; + } else if (value instanceof HttpServletResponse) { + continue; + } else if (value instanceof MultipartFile) { + MultipartFile multipartFile = (MultipartFile) value; + String name = multipartFile.getName(); + String fileName = multipartFile.getOriginalFilename(); + paraMap.put(name, fileName); + continue; + } else if (value instanceof MultipartFile[]) { + MultipartFile[] arr = (MultipartFile[]) value; + if (arr.length == 0) { + continue; + } + String name = arr[0].getName(); + StringBuilder sb = new StringBuilder(arr.length); + for (MultipartFile multipartFile : arr) { + sb.append(multipartFile.getOriginalFilename()); + sb.append(StringPool.COMMA); + } + paraMap.put(name, StringUtil.removeSuffix(sb.toString(), StringPool.COMMA)); + continue; + } else if (value instanceof List) { + List list = (List) value; + AtomicBoolean isSkip = new AtomicBoolean(false); + for (Object o : list) { + if ("StandardMultipartFile".equalsIgnoreCase(o.getClass().getSimpleName())) { + isSkip.set(true); + break; + } + } + if (isSkip.get()) { + paraMap.put(parameterName, "此参数不能序列化为json"); + continue; + } + } + // 参数名 + RequestParam requestParam = methodParam.getParameterAnnotation(RequestParam.class); + String paraName = parameterName; + if (requestParam != null && StringUtil.isNotBlank(requestParam.value())) { + paraName = requestParam.value(); + } + if (value == null) { + paraMap.put(paraName, null); + } else if (ClassUtil.isPrimitiveOrWrapper(value.getClass())) { + paraMap.put(paraName, value); + } else if (value instanceof InputStream) { + paraMap.put(paraName, "InputStream"); + } else if (value instanceof InputStreamSource) { + paraMap.put(paraName, "InputStreamSource"); + } else if (JsonUtil.canSerialize(value)) { + // 判断模型能被 json 序列化,则添加 + paraMap.put(paraName, value); + } else { + paraMap.put(paraName, "此参数不能序列化为json"); + } + } + // 请求参数 + if (paraMap.isEmpty()) { + beforeReqLog.append("\n"); + } else { + beforeReqLog.append(" Parameters: {}\n"); + beforeReqArgs.add(JsonUtil.toJson(paraMap)); + } + if (requestBodyValue != null) { + beforeReqLog.append("====Body===== {}\n"); + beforeReqArgs.add(JsonUtil.toJson(requestBodyValue)); + } + } + + /** + * 记录请求头 + * + * @param request HttpServletRequest + * @param level 日志级别 + * @param beforeReqLog StringBuilder + * @param beforeReqArgs beforeReqArgs + */ + public void logIngHeaders(HttpServletRequest request, BladeLogLevel level, + StringBuilder beforeReqLog, List beforeReqArgs) { + // 打印请求头 + if (BladeLogLevel.HEADERS.lte(level)) { + Enumeration headers = request.getHeaderNames(); + while (headers.hasMoreElements()) { + String headerName = headers.nextElement(); + String headerValue = request.getHeader(headerName); + beforeReqLog.append("===Headers=== {}: {}\n"); + beforeReqArgs.add(headerName); + beforeReqArgs.add(headerValue); + } + } + } + + private boolean isSkip(String path) { + return properties.getSkipUrl().stream().anyMatch(pattern -> antPathMatcher.match(pattern, path)); + } + +} diff --git a/blade-starter-log/src/main/java/org/springblade/core/log/config/BladeErrorMvcAutoConfiguration.java b/blade-starter-log/src/main/java/org/springblade/core/log/config/BladeErrorMvcAutoConfiguration.java new file mode 100644 index 0000000..700954c --- /dev/null +++ b/blade-starter-log/src/main/java/org/springblade/core/log/config/BladeErrorMvcAutoConfiguration.java @@ -0,0 +1,73 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.log.config; + + +import lombok.AllArgsConstructor; +import org.springblade.core.log.error.BladeErrorAttributes; +import org.springblade.core.log.error.BladeErrorController; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.autoconfigure.condition.SearchStrategy; +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController; +import org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration; +import org.springframework.boot.web.servlet.error.DefaultErrorAttributes; +import org.springframework.boot.web.servlet.error.ErrorAttributes; +import org.springframework.boot.web.servlet.error.ErrorController; +import org.springframework.context.annotation.Bean; +import org.springframework.web.servlet.DispatcherServlet; + +import jakarta.servlet.Servlet; + +/** + * 统一异常处理 + * + * @author Chill + */ +@AllArgsConstructor +@ConditionalOnWebApplication +@AutoConfiguration(before = ErrorMvcAutoConfiguration.class) +@ConditionalOnClass({Servlet.class, DispatcherServlet.class}) +public class BladeErrorMvcAutoConfiguration { + + private final ServerProperties serverProperties; + + @Bean + @ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT) + public DefaultErrorAttributes errorAttributes() { + return new BladeErrorAttributes(); + } + + @Bean + @ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT) + public BasicErrorController basicErrorController(ErrorAttributes errorAttributes) { + return new BladeErrorController(errorAttributes, serverProperties.getError()); + } + +} diff --git a/blade-starter-log/src/main/java/org/springblade/core/log/config/BladeLogToolAutoConfiguration.java b/blade-starter-log/src/main/java/org/springblade/core/log/config/BladeLogToolAutoConfiguration.java new file mode 100644 index 0000000..b729c84 --- /dev/null +++ b/blade-starter-log/src/main/java/org/springblade/core/log/config/BladeLogToolAutoConfiguration.java @@ -0,0 +1,106 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ + +package org.springblade.core.log.config; + +import org.springblade.core.launch.props.BladeProperties; +import org.springblade.core.launch.props.BladePropertySource; +import org.springblade.core.launch.server.ServerInfo; +import org.springblade.core.log.aspect.ApiLogAspect; +import org.springblade.core.log.aspect.LogTraceAspect; +import org.springblade.core.log.event.ApiLogListener; +import org.springblade.core.log.event.ErrorLogListener; +import org.springblade.core.log.event.UsualLogListener; +import org.springblade.core.log.feign.ILogClient; +import org.springblade.core.log.filter.LogTraceFilter; +import org.springblade.core.log.logger.BladeLogger; +import org.springblade.core.log.props.BladeRequestLogProperties; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.core.Ordered; + +import jakarta.servlet.DispatcherType; + +/** + * 日志工具自动配置 + * + * @author Chill + */ +@AutoConfiguration +@ConditionalOnWebApplication +@EnableConfigurationProperties(BladeRequestLogProperties.class) +@BladePropertySource(value = "classpath:/blade-log.yml") +public class BladeLogToolAutoConfiguration { + + @Bean + public ApiLogAspect apiLogAspect() { + return new ApiLogAspect(); + } + + @Bean + public LogTraceAspect logTraceAspect() { + return new LogTraceAspect(); + } + + @Bean + public BladeLogger bladeLogger() { + return new BladeLogger(); + } + + @Bean + public FilterRegistrationBean logTraceFilterRegistration() { + FilterRegistrationBean registration = new FilterRegistrationBean<>(); + registration.setDispatcherTypes(DispatcherType.REQUEST); + registration.setFilter(new LogTraceFilter()); + registration.addUrlPatterns("/*"); + registration.setName("LogTraceFilter"); + registration.setOrder(Ordered.LOWEST_PRECEDENCE); + return registration; + } + + @Bean + @ConditionalOnMissingBean(name = "apiLogListener") + public ApiLogListener apiLogListener(ILogClient logService, ServerInfo serverInfo, BladeProperties bladeProperties) { + return new ApiLogListener(logService, serverInfo, bladeProperties); + } + + @Bean + @ConditionalOnMissingBean(name = "errorEventListener") + public ErrorLogListener errorEventListener(ILogClient logService, ServerInfo serverInfo, BladeProperties bladeProperties) { + return new ErrorLogListener(logService, serverInfo, bladeProperties); + } + + @Bean + @ConditionalOnMissingBean(name = "usualEventListener") + public UsualLogListener usualEventListener(ILogClient logService, ServerInfo serverInfo, BladeProperties bladeProperties) { + return new UsualLogListener(logService, serverInfo, bladeProperties); + } + +} diff --git a/blade-starter-log/src/main/java/org/springblade/core/log/constant/EventConstant.java b/blade-starter-log/src/main/java/org/springblade/core/log/constant/EventConstant.java new file mode 100644 index 0000000..402ea40 --- /dev/null +++ b/blade-starter-log/src/main/java/org/springblade/core/log/constant/EventConstant.java @@ -0,0 +1,44 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.log.constant; + +/** + * 事件常量 + * + * @author Chill + */ +public interface EventConstant { + + /** + * log + */ + String EVENT_LOG = "log"; + /** + * request + */ + String EVENT_REQUEST = "request"; + +} diff --git a/blade-starter-log/src/main/java/org/springblade/core/log/error/BladeErrorAttributes.java b/blade-starter-log/src/main/java/org/springblade/core/log/error/BladeErrorAttributes.java new file mode 100644 index 0000000..6979372 --- /dev/null +++ b/blade-starter-log/src/main/java/org/springblade/core/log/error/BladeErrorAttributes.java @@ -0,0 +1,72 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.log.error; + +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.log.publisher.ErrorLogPublisher; +import org.springblade.core.tool.api.R; +import org.springblade.core.tool.api.ResultCode; +import org.springblade.core.tool.utils.BeanUtil; +import org.springframework.boot.web.error.ErrorAttributeOptions; +import org.springframework.boot.web.servlet.error.DefaultErrorAttributes; +import org.springframework.lang.Nullable; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.WebRequest; + +import java.util.Map; + +/** + * 全局异常处理 + * + * @author Chill + */ +@Slf4j +public class BladeErrorAttributes extends DefaultErrorAttributes { + + @Override + public Map getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) { + String requestUri = this.getAttr(webRequest, "jakarta.servlet.error.request_uri"); + Integer status = this.getAttr(webRequest, "jakarta.servlet.error.status_code"); + Throwable error = getError(webRequest); + R result; + if (error == null) { + log.error("URL:{} error status:{}", requestUri, status); + result = R.fail(ResultCode.FAILURE, "系统未知异常[HttpStatus]:" + status); + } else { + log.error(String.format("URL:%s error status:%d", requestUri, status), error); + result = R.fail(status, error.getMessage()); + } + //发送服务异常事件 + ErrorLogPublisher.publishEvent(error, requestUri); + return BeanUtil.toMap(result); + } + + @Nullable + private T getAttr(WebRequest webRequest, String name) { + return (T) webRequest.getAttribute(name, RequestAttributes.SCOPE_REQUEST); + } + +} diff --git a/blade-starter-log/src/main/java/org/springblade/core/log/error/BladeErrorController.java b/blade-starter-log/src/main/java/org/springblade/core/log/error/BladeErrorController.java new file mode 100644 index 0000000..07c0812 --- /dev/null +++ b/blade-starter-log/src/main/java/org/springblade/core/log/error/BladeErrorController.java @@ -0,0 +1,65 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.log.error; + +import org.springblade.core.tool.jackson.JsonUtil; +import org.springframework.boot.autoconfigure.web.ErrorProperties; +import org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController; +import org.springframework.boot.web.error.ErrorAttributeOptions; +import org.springframework.boot.web.servlet.error.ErrorAttributes; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.view.json.MappingJackson2JsonView; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.util.Map; + +/** + * 更改html请求异常为ajax + * + * @author Chill + */ +public class BladeErrorController extends BasicErrorController { + + public BladeErrorController(ErrorAttributes errorAttributes, ErrorProperties errorProperties) { + super(errorAttributes, errorProperties); + } + + @Override + public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) { + boolean includeStackTrace = isIncludeStackTrace(request, MediaType.ALL); + Map body = getErrorAttributes(request, (includeStackTrace) ? ErrorAttributeOptions.of(ErrorAttributeOptions.Include.STACK_TRACE) : ErrorAttributeOptions.defaults()); + HttpStatus status = getStatus(request); + response.setStatus(status.value()); + MappingJackson2JsonView view = new MappingJackson2JsonView(); + view.setObjectMapper(JsonUtil.getInstance()); + view.setContentType(MediaType.APPLICATION_JSON_VALUE); + return new ModelAndView(view, body); + } + +} diff --git a/blade-starter-log/src/main/java/org/springblade/core/log/error/BladeRestExceptionTranslator.java b/blade-starter-log/src/main/java/org/springblade/core/log/error/BladeRestExceptionTranslator.java new file mode 100644 index 0000000..db006de --- /dev/null +++ b/blade-starter-log/src/main/java/org/springblade/core/log/error/BladeRestExceptionTranslator.java @@ -0,0 +1,100 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.log.error; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.launch.props.BladeProperties; +import org.springblade.core.log.exception.ServiceException; +import org.springblade.core.log.props.BladeRequestLogProperties; +import org.springblade.core.log.publisher.ErrorLogPublisher; +import org.springblade.core.secure.exception.SecureException; +import org.springblade.core.tool.api.R; +import org.springblade.core.tool.api.ResultCode; +import org.springblade.core.tool.utils.Func; +import org.springblade.core.tool.utils.UrlUtil; +import org.springblade.core.tool.utils.WebUtil; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.core.annotation.Order; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.servlet.DispatcherServlet; + +import jakarta.servlet.Servlet; + +import java.util.Objects; + +/** + * 未知异常转译和发送,方便监听,对未知异常统一处理。Order 排序优先级低 + * + * @author Chill + */ +@Slf4j +@Order +@RequiredArgsConstructor +@AutoConfiguration +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) +@ConditionalOnClass({Servlet.class, DispatcherServlet.class}) +@RestControllerAdvice +public class BladeRestExceptionTranslator { + + private final BladeProperties bladeProperties; + private final BladeRequestLogProperties requestLogProperties; + + @ExceptionHandler(ServiceException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public R handleError(ServiceException e) { + log.error("业务异常", e); + return R.fail(e.getResultCode(), e.getMessage()); + } + + @ExceptionHandler(SecureException.class) + @ResponseStatus(HttpStatus.UNAUTHORIZED) + public R handleError(SecureException e) { + log.error("认证异常", e); + return R.fail(e.getResultCode(), e.getMessage()); + } + + @ExceptionHandler(Throwable.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public R handleError(Throwable e) { + log.error("服务器异常", e); + if (requestLogProperties.getErrorLog()) { + //发送服务异常事件 + ErrorLogPublisher.publishEvent(e, UrlUtil.getPath(Objects.requireNonNull(WebUtil.getRequest()).getRequestURI())); + } + // 生产环境屏蔽具体异常信息返回 + if (bladeProperties.isProd()) { + return R.fail(ResultCode.INTERNAL_SERVER_ERROR); + } + return R.fail(ResultCode.INTERNAL_SERVER_ERROR, (Func.isEmpty(e.getMessage()) ? ResultCode.INTERNAL_SERVER_ERROR.getMessage() : e.getMessage())); + } + +} diff --git a/blade-starter-log/src/main/java/org/springblade/core/log/error/RestExceptionTranslator.java b/blade-starter-log/src/main/java/org/springblade/core/log/error/RestExceptionTranslator.java new file mode 100644 index 0000000..7c09180 --- /dev/null +++ b/blade-starter-log/src/main/java/org/springblade/core/log/error/RestExceptionTranslator.java @@ -0,0 +1,157 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.log.error; + +import lombok.extern.slf4j.Slf4j; +import org.hibernate.validator.internal.engine.path.PathImpl; +import org.springblade.core.tool.api.R; +import org.springblade.core.tool.api.ResultCode; +import org.springblade.core.tool.utils.StringUtil; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.http.HttpStatus; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.validation.BindException; +import org.springframework.validation.BindingResult; +import org.springframework.validation.FieldError; +import org.springframework.web.HttpMediaTypeNotAcceptableException; +import org.springframework.web.HttpMediaTypeNotSupportedException; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import org.springframework.web.servlet.DispatcherServlet; +import org.springframework.web.servlet.NoHandlerFoundException; + +import jakarta.servlet.Servlet; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; + +import java.util.Set; + +/** + * 全局异常处理,处理可预见的异常,Order 排序优先级高 + * + * @author Chill + */ +@Slf4j +@Order(Ordered.HIGHEST_PRECEDENCE) +@AutoConfiguration +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) +@ConditionalOnClass({Servlet.class, DispatcherServlet.class}) +@RestControllerAdvice +public class RestExceptionTranslator { + + @ExceptionHandler(MissingServletRequestParameterException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public R handleError(MissingServletRequestParameterException e) { + log.warn("缺少请求参数", e.getMessage()); + String message = String.format("缺少必要的请求参数: %s", e.getParameterName()); + return R.fail(ResultCode.PARAM_MISS, message); + } + + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public R handleError(MethodArgumentTypeMismatchException e) { + log.warn("请求参数格式错误", e.getMessage()); + String message = String.format("请求参数格式错误: %s", e.getName()); + return R.fail(ResultCode.PARAM_TYPE_ERROR, message); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public R handleError(MethodArgumentNotValidException e) { + log.warn("参数验证失败", e.getMessage()); + return handleError(e.getBindingResult()); + } + + @ExceptionHandler(BindException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public R handleError(BindException e) { + log.warn("参数绑定失败", e.getMessage()); + return handleError(e.getBindingResult()); + } + + private R handleError(BindingResult result) { + FieldError error = result.getFieldError(); + String message = String.format("%s:%s", error.getField(), error.getDefaultMessage()); + return R.fail(ResultCode.PARAM_BIND_ERROR, message); + } + + @ExceptionHandler(ConstraintViolationException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public R handleError(ConstraintViolationException e) { + log.warn("参数验证失败", e.getMessage()); + Set> violations = e.getConstraintViolations(); + ConstraintViolation violation = violations.iterator().next(); + String path = ((PathImpl) violation.getPropertyPath()).getLeafNode().getName(); + String message = String.format("%s:%s", path, violation.getMessage()); + return R.fail(ResultCode.PARAM_VALID_ERROR, message); + } + + @ExceptionHandler(NoHandlerFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public R handleError(NoHandlerFoundException e) { + log.error("404没找到请求:{}", e.getMessage()); + return R.fail(ResultCode.NOT_FOUND, e.getMessage()); + } + + @ExceptionHandler(HttpMessageNotReadableException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public R handleError(HttpMessageNotReadableException e) { + log.error("消息不能读取:{}", e.getMessage()); + return R.fail(ResultCode.MSG_NOT_READABLE, e.getMessage()); + } + + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + @ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED) + public R handleError(HttpRequestMethodNotSupportedException e) { + log.error("不支持当前请求方法:{}", e.getMessage()); + return R.fail(ResultCode.METHOD_NOT_SUPPORTED, e.getMessage()); + } + + @ExceptionHandler(HttpMediaTypeNotSupportedException.class) + @ResponseStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE) + public R handleError(HttpMediaTypeNotSupportedException e) { + log.error("不支持当前媒体类型:{}", e.getMessage()); + return R.fail(ResultCode.MEDIA_TYPE_NOT_SUPPORTED, e.getMessage()); + } + + @ExceptionHandler(HttpMediaTypeNotAcceptableException.class) + @ResponseStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE) + public R handleError(HttpMediaTypeNotAcceptableException e) { + String message = e.getMessage() + " " + StringUtil.join(e.getSupportedMediaTypes()); + log.error("不接受的媒体类型:{}", message); + return R.fail(ResultCode.MEDIA_TYPE_NOT_SUPPORTED, message); + } + +} diff --git a/blade-starter-log/src/main/java/org/springblade/core/log/event/ApiLogEvent.java b/blade-starter-log/src/main/java/org/springblade/core/log/event/ApiLogEvent.java new file mode 100644 index 0000000..a07d882 --- /dev/null +++ b/blade-starter-log/src/main/java/org/springblade/core/log/event/ApiLogEvent.java @@ -0,0 +1,44 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ + +package org.springblade.core.log.event; + +import org.springframework.context.ApplicationEvent; + +import java.util.Map; + +/** + * 系统日志事件 + * + * @author Chill + */ +public class ApiLogEvent extends ApplicationEvent { + + public ApiLogEvent(Map source) { + super(source); + } + +} diff --git a/blade-starter-log/src/main/java/org/springblade/core/log/event/ApiLogListener.java b/blade-starter-log/src/main/java/org/springblade/core/log/event/ApiLogListener.java new file mode 100644 index 0000000..9dc75f9 --- /dev/null +++ b/blade-starter-log/src/main/java/org/springblade/core/log/event/ApiLogListener.java @@ -0,0 +1,68 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ + +package org.springblade.core.log.event; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.launch.props.BladeProperties; +import org.springblade.core.launch.server.ServerInfo; +import org.springblade.core.log.constant.EventConstant; +import org.springblade.core.log.feign.ILogClient; +import org.springblade.core.log.model.LogApi; +import org.springblade.core.log.utils.LogAbstractUtil; +import org.springframework.context.event.EventListener; +import org.springframework.core.annotation.Order; +import org.springframework.scheduling.annotation.Async; + +import java.util.Map; + + +/** + * 异步监听日志事件 + * + * @author Chill + */ +@Slf4j +@AllArgsConstructor +public class ApiLogListener { + + private final ILogClient logService; + private final ServerInfo serverInfo; + private final BladeProperties bladeProperties; + + + @Async + @Order + @EventListener(ApiLogEvent.class) + public void saveApiLog(ApiLogEvent event) { + Map source = (Map) event.getSource(); + LogApi logApi = (LogApi) source.get(EventConstant.EVENT_LOG); + LogAbstractUtil.addOtherInfoToLog(logApi, bladeProperties, serverInfo); + logService.saveApiLog(logApi); + } + +} diff --git a/blade-starter-log/src/main/java/org/springblade/core/log/event/ErrorLogEvent.java b/blade-starter-log/src/main/java/org/springblade/core/log/event/ErrorLogEvent.java new file mode 100644 index 0000000..e71e6a8 --- /dev/null +++ b/blade-starter-log/src/main/java/org/springblade/core/log/event/ErrorLogEvent.java @@ -0,0 +1,44 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.log.event; + + +import org.springframework.context.ApplicationEvent; + +import java.util.Map; + +/** + * 错误日志事件 + * + * @author Chill + */ +public class ErrorLogEvent extends ApplicationEvent { + + public ErrorLogEvent(Map source) { + super(source); + } + +} diff --git a/blade-starter-log/src/main/java/org/springblade/core/log/event/ErrorLogListener.java b/blade-starter-log/src/main/java/org/springblade/core/log/event/ErrorLogListener.java new file mode 100644 index 0000000..ca66ec8 --- /dev/null +++ b/blade-starter-log/src/main/java/org/springblade/core/log/event/ErrorLogListener.java @@ -0,0 +1,66 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.log.event; + + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.launch.props.BladeProperties; +import org.springblade.core.launch.server.ServerInfo; +import org.springblade.core.log.constant.EventConstant; +import org.springblade.core.log.feign.ILogClient; +import org.springblade.core.log.model.LogError; +import org.springblade.core.log.utils.LogAbstractUtil; +import org.springframework.context.event.EventListener; +import org.springframework.core.annotation.Order; +import org.springframework.scheduling.annotation.Async; + +import java.util.Map; + +/** + * 异步监听错误日志事件 + * + * @author Chill + */ +@Slf4j +@AllArgsConstructor +public class ErrorLogListener { + + private final ILogClient logService; + private final ServerInfo serverInfo; + private final BladeProperties bladeProperties; + + @Async + @Order + @EventListener(ErrorLogEvent.class) + public void saveErrorLog(ErrorLogEvent event) { + Map source = (Map) event.getSource(); + LogError logError = (LogError) source.get(EventConstant.EVENT_LOG); + LogAbstractUtil.addOtherInfoToLog(logError, bladeProperties, serverInfo); + logService.saveErrorLog(logError); + } + +} diff --git a/blade-starter-log/src/main/java/org/springblade/core/log/event/UsualLogEvent.java b/blade-starter-log/src/main/java/org/springblade/core/log/event/UsualLogEvent.java new file mode 100644 index 0000000..8c1d500 --- /dev/null +++ b/blade-starter-log/src/main/java/org/springblade/core/log/event/UsualLogEvent.java @@ -0,0 +1,44 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ + +package org.springblade.core.log.event; + +import org.springframework.context.ApplicationEvent; + +import java.util.Map; + +/** + * 系统日志事件 + * + * @author Chill + */ +public class UsualLogEvent extends ApplicationEvent { + + public UsualLogEvent(Map source) { + super(source); + } + +} diff --git a/blade-starter-log/src/main/java/org/springblade/core/log/event/UsualLogListener.java b/blade-starter-log/src/main/java/org/springblade/core/log/event/UsualLogListener.java new file mode 100644 index 0000000..c42a515 --- /dev/null +++ b/blade-starter-log/src/main/java/org/springblade/core/log/event/UsualLogListener.java @@ -0,0 +1,66 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.log.event; + + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.launch.props.BladeProperties; +import org.springblade.core.launch.server.ServerInfo; +import org.springblade.core.log.constant.EventConstant; +import org.springblade.core.log.feign.ILogClient; +import org.springblade.core.log.model.LogUsual; +import org.springblade.core.log.utils.LogAbstractUtil; +import org.springframework.context.event.EventListener; +import org.springframework.core.annotation.Order; +import org.springframework.scheduling.annotation.Async; + +import java.util.Map; + +/** + * 异步监听日志事件 + * + * @author Chill + */ +@Slf4j +@AllArgsConstructor +public class UsualLogListener { + + private final ILogClient logService; + private final ServerInfo serverInfo; + private final BladeProperties bladeProperties; + + @Async + @Order + @EventListener(UsualLogEvent.class) + public void saveUsualLog(UsualLogEvent event) { + Map source = (Map) event.getSource(); + LogUsual logUsual = (LogUsual) source.get(EventConstant.EVENT_LOG); + LogAbstractUtil.addOtherInfoToLog(logUsual, bladeProperties, serverInfo); + logService.saveUsualLog(logUsual); + } + +} diff --git a/blade-starter-log/src/main/java/org/springblade/core/log/exception/ServiceException.java b/blade-starter-log/src/main/java/org/springblade/core/log/exception/ServiceException.java new file mode 100644 index 0000000..412d4cb --- /dev/null +++ b/blade-starter-log/src/main/java/org/springblade/core/log/exception/ServiceException.java @@ -0,0 +1,73 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.log.exception; + +import lombok.Getter; +import org.springblade.core.tool.api.IResultCode; +import org.springblade.core.tool.api.ResultCode; + + +/** + * 业务异常 + * + * @author Chill + */ +public class ServiceException extends RuntimeException { + private static final long serialVersionUID = 2359767895161832954L; + + @Getter + private final IResultCode resultCode; + + public ServiceException(String message) { + super(message); + this.resultCode = ResultCode.FAILURE; + } + + public ServiceException(IResultCode resultCode) { + super(resultCode.getMessage()); + this.resultCode = resultCode; + } + + public ServiceException(IResultCode resultCode, Throwable cause) { + super(cause); + this.resultCode = resultCode; + } + + /** + * 提高性能 + * + * @return Throwable + */ + @Override + public Throwable fillInStackTrace() { + return this; + } + + public Throwable doFillInStackTrace() { + return super.fillInStackTrace(); + } + +} diff --git a/blade-starter-log/src/main/java/org/springblade/core/log/feign/ILogClient.java b/blade-starter-log/src/main/java/org/springblade/core/log/feign/ILogClient.java new file mode 100644 index 0000000..379845c --- /dev/null +++ b/blade-starter-log/src/main/java/org/springblade/core/log/feign/ILogClient.java @@ -0,0 +1,77 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.log.feign; + +import org.springblade.core.launch.constant.AppConstant; +import org.springblade.core.log.model.LogApi; +import org.springblade.core.log.model.LogUsual; +import org.springblade.core.log.model.LogError; +import org.springblade.core.tool.api.R; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; + +/** + * Feign接口类 + * + * @author Chill + */ +@FeignClient( + value = AppConstant.APPLICATION_LOG_NAME, + fallback = LogClientFallback.class +) +public interface ILogClient { + + String API_PREFIX = "/log"; + + /** + * 保存错误日志 + * + * @param log + * @return + */ + @PostMapping(API_PREFIX + "/saveUsualLog") + R saveUsualLog(@RequestBody LogUsual log); + + /** + * 保存操作日志 + * + * @param log + * @return + */ + @PostMapping(API_PREFIX + "/saveApiLog") + R saveApiLog(@RequestBody LogApi log); + + /** + * 保存错误日志 + * + * @param log + * @return + */ + @PostMapping(API_PREFIX + "/saveErrorLog") + R saveErrorLog(@RequestBody LogError log); + +} diff --git a/blade-starter-log/src/main/java/org/springblade/core/log/feign/LogClientFallback.java b/blade-starter-log/src/main/java/org/springblade/core/log/feign/LogClientFallback.java new file mode 100644 index 0000000..60acddc --- /dev/null +++ b/blade-starter-log/src/main/java/org/springblade/core/log/feign/LogClientFallback.java @@ -0,0 +1,58 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.log.feign; + +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.log.model.LogApi; +import org.springblade.core.log.model.LogError; +import org.springblade.core.log.model.LogUsual; +import org.springblade.core.tool.api.R; +import org.springframework.stereotype.Component; + +/** + * 日志fallback + * + * @author jiang + */ +@Slf4j +@Component +public class LogClientFallback implements ILogClient { + + @Override + public R saveUsualLog(LogUsual log) { + return R.fail("usual log send fail"); + } + + @Override + public R saveApiLog(LogApi log) { + return R.fail("api log send fail"); + } + + @Override + public R saveErrorLog(LogError log) { + return R.fail("error log send fail"); + } +} diff --git a/blade-starter-log/src/main/java/org/springblade/core/log/filter/LogTraceFilter.java b/blade-starter-log/src/main/java/org/springblade/core/log/filter/LogTraceFilter.java new file mode 100644 index 0000000..bd89a45 --- /dev/null +++ b/blade-starter-log/src/main/java/org/springblade/core/log/filter/LogTraceFilter.java @@ -0,0 +1,60 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.log.filter; + +import org.springblade.core.log.utils.LogTraceUtil; + +import jakarta.servlet.*; +import java.io.IOException; + +/** + * 日志追踪过滤器 + * + * @author Chill + */ +public class LogTraceFilter implements Filter { + + @Override + public void init(FilterConfig filterConfig) { + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + boolean flag = LogTraceUtil.insert(); + try { + chain.doFilter(request, response); + } finally { + if (flag) { + LogTraceUtil.remove(); + } + } + } + + @Override + public void destroy() { + } + +} diff --git a/blade-starter-log/src/main/java/org/springblade/core/log/launch/LogLauncherServiceImpl.java b/blade-starter-log/src/main/java/org/springblade/core/log/launch/LogLauncherServiceImpl.java new file mode 100644 index 0000000..8853af0 --- /dev/null +++ b/blade-starter-log/src/main/java/org/springblade/core/log/launch/LogLauncherServiceImpl.java @@ -0,0 +1,52 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.log.launch; + +import org.springblade.core.auto.service.AutoService; +import org.springblade.core.launch.service.LauncherService; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.core.Ordered; + +import java.util.Properties; + +/** + * 日志启动配置类 + * + * @author Chill + */ +@AutoService(LauncherService.class) +public class LogLauncherServiceImpl implements LauncherService { + @Override + public void launcher(SpringApplicationBuilder builder, String appName, String profile, boolean isLocalDev) { + Properties props = System.getProperties(); + props.setProperty("logging.config", "classpath:log/logback-" + profile + ".xml"); + } + + @Override + public int getOrder() { + return Ordered.HIGHEST_PRECEDENCE; + } +} diff --git a/blade-starter-log/src/main/java/org/springblade/core/log/listener/LoggerStartupListener.java b/blade-starter-log/src/main/java/org/springblade/core/log/listener/LoggerStartupListener.java new file mode 100644 index 0000000..c2d48bc --- /dev/null +++ b/blade-starter-log/src/main/java/org/springblade/core/log/listener/LoggerStartupListener.java @@ -0,0 +1,97 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.log.listener; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.spi.LoggerContextListener; +import ch.qos.logback.core.Context; +import ch.qos.logback.core.spi.ContextAwareBase; +import ch.qos.logback.core.spi.LifeCycle; +import org.springblade.core.log.utils.ElkPropsUtil; +import org.springblade.core.tool.utils.StringUtil; + +/** + * logback监听类 + * + * @author Chill + */ +public class LoggerStartupListener extends ContextAwareBase implements LoggerContextListener, LifeCycle { + + @Override + public void start() { + Context context = getContext(); + context.putProperty("ELK_MODE", "FALSE"); + context.putProperty("STDOUT_APPENDER", "STDOUT"); + context.putProperty("INFO_APPENDER", "INFO"); + context.putProperty("ERROR_APPENDER", "ERROR"); + context.putProperty("DESTINATION", "127.0.0.1:9000"); + String destination = ElkPropsUtil.getDestination(); + if (StringUtil.isNotBlank(destination)) { + context.putProperty("ELK_MODE", "TRUE"); + context.putProperty("STDOUT_APPENDER", "STDOUT_LOGSTASH"); + context.putProperty("INFO_APPENDER", "INFO_LOGSTASH"); + context.putProperty("ERROR_APPENDER", "ERROR_LOGSTASH"); + context.putProperty("DESTINATION", destination); + } + } + + @Override + public void stop() { + + } + + @Override + public boolean isStarted() { + return false; + } + + @Override + public boolean isResetResistant() { + return false; + } + + @Override + public void onStart(LoggerContext context) { + + } + + @Override + public void onReset(LoggerContext context) { + + } + + @Override + public void onStop(LoggerContext context) { + + } + + @Override + public void onLevelChange(Logger logger, Level level) { + + } +} diff --git a/blade-starter-log/src/main/java/org/springblade/core/log/logger/BladeLogger.java b/blade-starter-log/src/main/java/org/springblade/core/log/logger/BladeLogger.java new file mode 100644 index 0000000..398a607 --- /dev/null +++ b/blade-starter-log/src/main/java/org/springblade/core/log/logger/BladeLogger.java @@ -0,0 +1,65 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.log.logger; + +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.log.publisher.UsualLogPublisher; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Value; + +/** + * 日志工具类 + * + * @author Chill + */ +@Slf4j +public class BladeLogger implements InitializingBean { + + @Value("${spring.application.name}") + private String serviceId; + + public void info(String id, String data) { + UsualLogPublisher.publishEvent("info", id, data); + } + + public void debug(String id, String data) { + UsualLogPublisher.publishEvent("debug", id, data); + } + + public void warn(String id, String data) { + UsualLogPublisher.publishEvent("warn", id, data); + } + + public void error(String id, String data) { + UsualLogPublisher.publishEvent("error", id, data); + } + + @Override + public void afterPropertiesSet() throws Exception { + log.info(serviceId + ": BladeLogger init success!"); + } + +} diff --git a/blade-starter-log/src/main/java/org/springblade/core/log/model/LogAbstract.java b/blade-starter-log/src/main/java/org/springblade/core/log/model/LogAbstract.java new file mode 100644 index 0000000..ae06548 --- /dev/null +++ b/blade-starter-log/src/main/java/org/springblade/core/log/model/LogAbstract.java @@ -0,0 +1,116 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.log.model; + + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import lombok.Data; +import org.springblade.core.tool.utils.DateUtil; +import org.springframework.format.annotation.DateTimeFormat; + +import java.io.Serializable; +import java.util.Date; + +/** + * logApi、logError、logUsual父类 + * + * @author Chill + */ +@Data +public class LogAbstract implements Serializable { + + protected static final long serialVersionUID = 1L; + + /** + * 主键id + */ + @JsonSerialize(using = ToStringSerializer.class) + @TableId(value = "id", type = IdType.ASSIGN_ID) + protected Long id; + /** + * 租户ID + */ + private String tenantId; + /** + * 服务ID + */ + protected String serviceId; + /** + * 服务器 ip + */ + protected String serverIp; + /** + * 服务器名 + */ + protected String serverHost; + /** + * 环境 + */ + protected String env; + /** + * 操作IP地址 + */ + protected String remoteIp; + /** + * 用户代理 + */ + protected String userAgent; + /** + * 请求URI + */ + protected String requestUri; + /** + * 操作方式 + */ + protected String method; + /** + * 方法类 + */ + protected String methodClass; + /** + * 方法名 + */ + protected String methodName; + /** + * 操作提交的数据 + */ + protected String params; + /** + * 创建人 + */ + protected String createBy; + /** + * 创建时间 + */ + @DateTimeFormat(pattern = DateUtil.PATTERN_DATETIME) + @JsonFormat(pattern = DateUtil.PATTERN_DATETIME) + protected Date createTime; + +} diff --git a/blade-starter-log/src/main/java/org/springblade/core/log/model/LogApi.java b/blade-starter-log/src/main/java/org/springblade/core/log/model/LogApi.java new file mode 100644 index 0000000..80d4838 --- /dev/null +++ b/blade-starter-log/src/main/java/org/springblade/core/log/model/LogApi.java @@ -0,0 +1,62 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.log.model; + + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serial; + +/** + * 实体类 + * + * @author Chill + */ +@Data +@TableName("blade_log_api") +@EqualsAndHashCode(callSuper = true) +public class LogApi extends LogAbstract { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 日志类型 + */ + private String type; + /** + * 日志标题 + */ + private String title; + /** + * 执行时间 + */ + private String time; + + +} diff --git a/blade-starter-log/src/main/java/org/springblade/core/log/model/LogError.java b/blade-starter-log/src/main/java/org/springblade/core/log/model/LogError.java new file mode 100644 index 0000000..9856770 --- /dev/null +++ b/blade-starter-log/src/main/java/org/springblade/core/log/model/LogError.java @@ -0,0 +1,70 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.log.model; + + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serial; + +/** + * 服务 异常 + * + * @author Chill + */ +@Data +@TableName("blade_log_error") +@EqualsAndHashCode(callSuper = true) +public class LogError extends LogAbstract { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 堆栈信息 + */ + private String stackTrace; + /** + * 异常名 + */ + private String exceptionName; + /** + * 异常消息 + */ + private String message; + + /** + * 文件名 + */ + private String fileName; + + /** + * 代码行数 + */ + private Integer lineNumber; +} diff --git a/blade-starter-log/src/main/java/org/springblade/core/log/model/LogUsual.java b/blade-starter-log/src/main/java/org/springblade/core/log/model/LogUsual.java new file mode 100644 index 0000000..ebddd30 --- /dev/null +++ b/blade-starter-log/src/main/java/org/springblade/core/log/model/LogUsual.java @@ -0,0 +1,62 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.log.model; + + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serial; + +/** + * 实体类 + * + * @author Chill + */ +@Data +@TableName("blade_log_usual") +@EqualsAndHashCode(callSuper = true) +public class LogUsual extends LogAbstract { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 日志级别 + */ + private String logLevel; + /** + * 日志业务id + */ + private String logId; + /** + * 日志数据 + */ + private String logData; + + +} diff --git a/blade-starter-log/src/main/java/org/springblade/core/log/props/BladeRequestLogProperties.java b/blade-starter-log/src/main/java/org/springblade/core/log/props/BladeRequestLogProperties.java new file mode 100644 index 0000000..5a442d8 --- /dev/null +++ b/blade-starter-log/src/main/java/org/springblade/core/log/props/BladeRequestLogProperties.java @@ -0,0 +1,67 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.log.props; + +import lombok.Getter; +import lombok.Setter; +import org.springblade.core.launch.log.BladeLogLevel; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.cloud.context.config.annotation.RefreshScope; + +import java.util.ArrayList; +import java.util.List; + +/** + * 日志配置 + * + * @author L.cm + */ +@Getter +@Setter +@RefreshScope +@ConfigurationProperties(BladeLogLevel.REQ_LOG_PROPS_PREFIX) +public class BladeRequestLogProperties { + + /** + * 是否开启请求日志 + */ + private Boolean enabled = true; + + /** + * 是否开启异常日志推送 + */ + private Boolean errorLog = true; + + /** + * 日志级别配置,默认:BODY + */ + private BladeLogLevel level = BladeLogLevel.BODY; + + /** + * 放行url + */ + private List skipUrl = new ArrayList<>(); +} diff --git a/blade-starter-log/src/main/java/org/springblade/core/log/publisher/ApiLogPublisher.java b/blade-starter-log/src/main/java/org/springblade/core/log/publisher/ApiLogPublisher.java new file mode 100644 index 0000000..f8df341 --- /dev/null +++ b/blade-starter-log/src/main/java/org/springblade/core/log/publisher/ApiLogPublisher.java @@ -0,0 +1,63 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ + +package org.springblade.core.log.publisher; + +import org.springblade.core.log.annotation.ApiLog; +import org.springblade.core.log.constant.EventConstant; +import org.springblade.core.log.event.ApiLogEvent; +import org.springblade.core.log.model.LogApi; +import org.springblade.core.log.utils.LogAbstractUtil; +import org.springblade.core.tool.constant.BladeConstant; +import org.springblade.core.tool.utils.SpringUtil; +import org.springblade.core.tool.utils.WebUtil; + +import jakarta.servlet.http.HttpServletRequest; +import java.util.HashMap; +import java.util.Map; + +/** + * API日志信息事件发送 + * + * @author Chill + */ +public class ApiLogPublisher { + + public static void publishEvent(String methodName, String methodClass, ApiLog apiLog, long time) { + HttpServletRequest request = WebUtil.getRequest(); + LogApi logApi = new LogApi(); + logApi.setType(BladeConstant.LOG_NORMAL_TYPE); + logApi.setTitle(apiLog.value()); + logApi.setTime(String.valueOf(time)); + logApi.setMethodClass(methodClass); + logApi.setMethodName(methodName); + LogAbstractUtil.addRequestInfoToLog(request, logApi); + Map event = new HashMap<>(16); + event.put(EventConstant.EVENT_LOG, logApi); + SpringUtil.publishEvent(new ApiLogEvent(event)); + } + +} diff --git a/blade-starter-log/src/main/java/org/springblade/core/log/publisher/ErrorLogPublisher.java b/blade-starter-log/src/main/java/org/springblade/core/log/publisher/ErrorLogPublisher.java new file mode 100644 index 0000000..73a0b84 --- /dev/null +++ b/blade-starter-log/src/main/java/org/springblade/core/log/publisher/ErrorLogPublisher.java @@ -0,0 +1,72 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ + +package org.springblade.core.log.publisher; + +import org.springblade.core.log.constant.EventConstant; +import org.springblade.core.log.event.ErrorLogEvent; +import org.springblade.core.log.model.LogError; +import org.springblade.core.log.utils.LogAbstractUtil; +import org.springblade.core.tool.utils.Exceptions; +import org.springblade.core.tool.utils.Func; +import org.springblade.core.tool.utils.SpringUtil; +import org.springblade.core.tool.utils.WebUtil; + +import jakarta.servlet.http.HttpServletRequest; +import java.util.HashMap; +import java.util.Map; + +/** + * 异常信息事件发送 + * + * @author Chill + */ +public class ErrorLogPublisher { + + public static void publishEvent(Throwable error, String requestUri) { + HttpServletRequest request = WebUtil.getRequest(); + LogError logError = new LogError(); + logError.setRequestUri(requestUri); + if (Func.isNotEmpty(error)) { + logError.setStackTrace(Exceptions.getStackTraceAsString(error)); + logError.setExceptionName(error.getClass().getName()); + logError.setMessage(error.getMessage()); + StackTraceElement[] elements = error.getStackTrace(); + if (Func.isNotEmpty(elements)) { + StackTraceElement element = elements[0]; + logError.setMethodName(element.getMethodName()); + logError.setMethodClass(element.getClassName()); + logError.setFileName(element.getFileName()); + logError.setLineNumber(element.getLineNumber()); + } + } + LogAbstractUtil.addRequestInfoToLog(request, logError); + Map event = new HashMap<>(16); + event.put(EventConstant.EVENT_LOG, logError); + SpringUtil.publishEvent(new ErrorLogEvent(event)); + } + +} diff --git a/blade-starter-log/src/main/java/org/springblade/core/log/publisher/UsualLogPublisher.java b/blade-starter-log/src/main/java/org/springblade/core/log/publisher/UsualLogPublisher.java new file mode 100644 index 0000000..86a3b30 --- /dev/null +++ b/blade-starter-log/src/main/java/org/springblade/core/log/publisher/UsualLogPublisher.java @@ -0,0 +1,65 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ + +package org.springblade.core.log.publisher; + +import org.springblade.core.log.constant.EventConstant; +import org.springblade.core.log.event.UsualLogEvent; +import org.springblade.core.log.model.LogUsual; +import org.springblade.core.log.utils.LogAbstractUtil; +import org.springblade.core.tool.utils.SpringUtil; +import org.springblade.core.tool.utils.WebUtil; + +import jakarta.servlet.http.HttpServletRequest; +import java.util.HashMap; +import java.util.Map; + +/** + * BLADE日志信息事件发送 + * + * @author Chill + */ +public class UsualLogPublisher { + + public static void publishEvent(String level, String id, String data) { + HttpServletRequest request = WebUtil.getRequest(); + LogUsual logUsual = new LogUsual(); + logUsual.setLogLevel(level); + logUsual.setLogId(id); + logUsual.setLogData(data); + Thread thread = Thread.currentThread(); + StackTraceElement[] trace = thread.getStackTrace(); + if (trace.length > 3) { + logUsual.setMethodClass(trace[3].getClassName()); + logUsual.setMethodName(trace[3].getMethodName()); + } + LogAbstractUtil.addRequestInfoToLog(request, logUsual); + Map event = new HashMap<>(16); + event.put(EventConstant.EVENT_LOG, logUsual); + SpringUtil.publishEvent(new UsualLogEvent(event)); + } + +} diff --git a/blade-starter-log/src/main/java/org/springblade/core/log/utils/ElkPropsUtil.java b/blade-starter-log/src/main/java/org/springblade/core/log/utils/ElkPropsUtil.java new file mode 100644 index 0000000..2d3d878 --- /dev/null +++ b/blade-starter-log/src/main/java/org/springblade/core/log/utils/ElkPropsUtil.java @@ -0,0 +1,49 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.log.utils; + +import org.springblade.core.tool.utils.StringPool; + +import java.util.Properties; + +/** + * Elk配置工具 + * + * @author Chill + */ +public class ElkPropsUtil { + + /** + * 获取elk服务地址 + * + * @return 服务地址 + */ + public static String getDestination() { + Properties props = System.getProperties(); + return props.getProperty("blade.log.elk.destination", StringPool.EMPTY); + } + +} diff --git a/blade-starter-log/src/main/java/org/springblade/core/log/utils/LogAbstractUtil.java b/blade-starter-log/src/main/java/org/springblade/core/log/utils/LogAbstractUtil.java new file mode 100644 index 0000000..24f84c7 --- /dev/null +++ b/blade-starter-log/src/main/java/org/springblade/core/log/utils/LogAbstractUtil.java @@ -0,0 +1,80 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ + +package org.springblade.core.log.utils; + +import org.springblade.core.launch.props.BladeProperties; +import org.springblade.core.launch.server.ServerInfo; +import org.springblade.core.log.model.LogAbstract; +import org.springblade.core.secure.utils.AuthUtil; +import org.springblade.core.tool.constant.BladeConstant; +import org.springblade.core.tool.utils.*; + +import jakarta.servlet.http.HttpServletRequest; + +/** + * Log 相关工具 + * + * @author Chill + */ +public class LogAbstractUtil { + + /** + * 向log中添加补齐request的信息 + * + * @param request 请求 + * @param logAbstract 日志基础类 + */ + public static void addRequestInfoToLog(HttpServletRequest request, LogAbstract logAbstract) { + if (ObjectUtil.isNotEmpty(request)) { + logAbstract.setTenantId(Func.toStrWithEmpty(AuthUtil.getTenantId(), BladeConstant.ADMIN_TENANT_ID)); + logAbstract.setRemoteIp(WebUtil.getIP(request)); + logAbstract.setUserAgent(request.getHeader(WebUtil.USER_AGENT_HEADER)); + logAbstract.setRequestUri(UrlUtil.getPath(request.getRequestURI())); + logAbstract.setMethod(request.getMethod()); + logAbstract.setParams(WebUtil.getRequestContent(request)); + logAbstract.setCreateBy(AuthUtil.getUserAccount(request)); + } + } + + /** + * 向log中添加补齐其他的信息(eg:blade、server等) + * + * @param logAbstract 日志基础类 + * @param bladeProperties 配置信息 + * @param serverInfo 服务信息 + */ + public static void addOtherInfoToLog(LogAbstract logAbstract, BladeProperties bladeProperties, ServerInfo serverInfo) { + logAbstract.setServiceId(bladeProperties.getName()); + logAbstract.setServerHost(serverInfo.getHostName()); + logAbstract.setServerIp(serverInfo.getIpWithPort()); + logAbstract.setEnv(bladeProperties.getEnv()); + logAbstract.setCreateTime(DateUtil.now()); + if (logAbstract.getParams() == null) { + logAbstract.setParams(StringPool.EMPTY); + } + } +} diff --git a/blade-starter-log/src/main/java/org/springblade/core/log/utils/LogTraceUtil.java b/blade-starter-log/src/main/java/org/springblade/core/log/utils/LogTraceUtil.java new file mode 100644 index 0000000..decc846 --- /dev/null +++ b/blade-starter-log/src/main/java/org/springblade/core/log/utils/LogTraceUtil.java @@ -0,0 +1,62 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.log.utils; + +import org.slf4j.MDC; +import org.springblade.core.tool.utils.StringUtil; + +/** + * 日志追踪工具类 + * + * @author Chill + */ +public class LogTraceUtil { + private static final String UNIQUE_ID = "traceId"; + + /** + * 获取日志追踪id格式 + */ + public static String getTraceId() { + return StringUtil.randomUUID(); + } + + /** + * 插入traceId + */ + public static boolean insert() { + MDC.put(UNIQUE_ID, getTraceId()); + return true; + } + + /** + * 移除traceId + */ + public static boolean remove() { + MDC.remove(UNIQUE_ID); + return true; + } + +} diff --git a/blade-starter-log/src/main/resources/blade-log.yml b/blade-starter-log/src/main/resources/blade-log.yml new file mode 100644 index 0000000..0ae5269 --- /dev/null +++ b/blade-starter-log/src/main/resources/blade-log.yml @@ -0,0 +1,3 @@ +#配置日志地址 +logging: + config: classpath:log/logback-${blade.env}.xml diff --git a/blade-starter-log/src/main/resources/log/logback-dev.xml b/blade-starter-log/src/main/resources/log/logback-dev.xml new file mode 100644 index 0000000..9172296 --- /dev/null +++ b/blade-starter-log/src/main/resources/log/logback-dev.xml @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + ${CONSOLE_LOG_PATTERN} + utf8 + + + + + + + + ${DESTINATION} + + + + + UTC + + + + { + "traceId": "%X{traceId}", + "requestId": "%X{requestId}", + "accountId": "%X{accountId}", + "tenantId": "%X{tenantId}", + "logLevel": "%level", + "serviceName": "${springAppName:-SpringApp}", + "pid": "${PID:-}", + "thread": "%thread", + "class": "%logger{40}", + "line":"%L", + "message": "%message" + } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/blade-starter-log/src/main/resources/log/logback-prod.xml b/blade-starter-log/src/main/resources/log/logback-prod.xml new file mode 100644 index 0000000..a8f42de --- /dev/null +++ b/blade-starter-log/src/main/resources/log/logback-prod.xml @@ -0,0 +1,151 @@ + + + + + + + + + + + + + + + + ${CONSOLE_LOG_PATTERN} + utf8 + + + + + + + + target/blade/log/info-%d{yyyy-MM-dd}.log + + + %n%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] [%logger{50}] %n%-5level: %msg%n + + + + INFO + ACCEPT + DENY + + + + + + + + target/blade/log/error-%d{yyyy-MM-dd}.log + + + %n%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] [%logger{50}] %n%-5level: %msg%n + + + + ERROR + ACCEPT + DENY + + + + + + + + ${DESTINATION} + + + + + UTC + + + + { + "traceId": "%X{traceId}", + "requestId": "%X{requestId}", + "accountId": "%X{accountId}", + "tenantId": "%X{tenantId}", + "logLevel": "%level", + "serviceName": "${springAppName:-SpringApp}", + "pid": "${PID:-}", + "thread": "%thread", + "class": "%logger{40}", + "line":"%L", + "message": "%message" + } + + + + + + + + + INFO + ACCEPT + DENY + + + + + + ${DESTINATION} + + + + + UTC + + + + { + "traceId": "%X{traceId}", + "requestId": "%X{requestId}", + "accountId": "%X{accountId}", + "tenantId": "%X{tenantId}", + "logLevel": "%level", + "serviceName": "${springAppName:-SpringApp}", + "pid": "${PID:-}", + "thread": "%thread", + "class": "%logger{40}", + "line":"%L", + "message": "%message" + } + + + + + + + + + ERROR + ACCEPT + DENY + + + + + + + + + + + + + + + + + + + diff --git a/blade-starter-log/src/main/resources/log/logback-test.xml b/blade-starter-log/src/main/resources/log/logback-test.xml new file mode 100644 index 0000000..a8f42de --- /dev/null +++ b/blade-starter-log/src/main/resources/log/logback-test.xml @@ -0,0 +1,151 @@ + + + + + + + + + + + + + + + + ${CONSOLE_LOG_PATTERN} + utf8 + + + + + + + + target/blade/log/info-%d{yyyy-MM-dd}.log + + + %n%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] [%logger{50}] %n%-5level: %msg%n + + + + INFO + ACCEPT + DENY + + + + + + + + target/blade/log/error-%d{yyyy-MM-dd}.log + + + %n%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] [%logger{50}] %n%-5level: %msg%n + + + + ERROR + ACCEPT + DENY + + + + + + + + ${DESTINATION} + + + + + UTC + + + + { + "traceId": "%X{traceId}", + "requestId": "%X{requestId}", + "accountId": "%X{accountId}", + "tenantId": "%X{tenantId}", + "logLevel": "%level", + "serviceName": "${springAppName:-SpringApp}", + "pid": "${PID:-}", + "thread": "%thread", + "class": "%logger{40}", + "line":"%L", + "message": "%message" + } + + + + + + + + + INFO + ACCEPT + DENY + + + + + + ${DESTINATION} + + + + + UTC + + + + { + "traceId": "%X{traceId}", + "requestId": "%X{requestId}", + "accountId": "%X{accountId}", + "tenantId": "%X{tenantId}", + "logLevel": "%level", + "serviceName": "${springAppName:-SpringApp}", + "pid": "${PID:-}", + "thread": "%thread", + "class": "%logger{40}", + "line":"%L", + "message": "%message" + } + + + + + + + + + ERROR + ACCEPT + DENY + + + + + + + + + + + + + + + + + + + diff --git a/blade-starter-metrics/pom.xml b/blade-starter-metrics/pom.xml new file mode 100644 index 0000000..72e9c4d --- /dev/null +++ b/blade-starter-metrics/pom.xml @@ -0,0 +1,57 @@ + + + 4.0.0 + + BladeX-Tool + org.springblade + ${revision} + + + blade-starter-metrics + ${project.artifactId} + ${project.parent.version} + jar + + + + org.springframework + spring-web + provided + + + org.springframework.boot + spring-boot-actuator-autoconfigure + provided + + + org.springframework.boot + spring-boot-starter-undertow + provided + + + io.micrometer + micrometer-core + + + io.micrometer + micrometer-registry-prometheus + + + com.alibaba.csp + sentinel-core + + + com.alibaba + druid + + + + org.springblade + blade-core-auto + provided + + + + diff --git a/blade-starter-metrics/src/main/java/org/springblade/core/metrics/druid/DruidDataSourcePoolMetadata.java b/blade-starter-metrics/src/main/java/org/springblade/core/metrics/druid/DruidDataSourcePoolMetadata.java new file mode 100644 index 0000000..afe51a3 --- /dev/null +++ b/blade-starter-metrics/src/main/java/org/springblade/core/metrics/druid/DruidDataSourcePoolMetadata.java @@ -0,0 +1,68 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.metrics.druid; + +import com.alibaba.druid.pool.DruidDataSource; +import org.springframework.boot.jdbc.metadata.AbstractDataSourcePoolMetadata; + +/** + * druid 连接池 pool meta data + * + * @author L.cm + */ +public class DruidDataSourcePoolMetadata extends AbstractDataSourcePoolMetadata { + + public DruidDataSourcePoolMetadata(DruidDataSource dataSource) { + super(dataSource); + } + + @Override + public Integer getActive() { + return getDataSource().getActiveCount(); + } + + @Override + public Integer getMax() { + return getDataSource().getMaxActive(); + } + + @Override + public Integer getMin() { + return getDataSource().getMinIdle(); + } + + @Override + public String getValidationQuery() { + return getDataSource().getValidationQuery(); + } + + @Override + public Boolean getDefaultAutoCommit() { + return getDataSource().isDefaultAutoCommit(); + } + +} diff --git a/blade-starter-metrics/src/main/java/org/springblade/core/metrics/druid/DruidMetrics.java b/blade-starter-metrics/src/main/java/org/springblade/core/metrics/druid/DruidMetrics.java new file mode 100644 index 0000000..8c5cd2e --- /dev/null +++ b/blade-starter-metrics/src/main/java/org/springblade/core/metrics/druid/DruidMetrics.java @@ -0,0 +1,136 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.metrics.druid; + +import com.alibaba.druid.pool.DruidDataSource; +import com.alibaba.druid.stat.JdbcConnectionStat; +import com.alibaba.druid.stat.JdbcDataSourceStat; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.binder.BaseUnits; +import io.micrometer.core.instrument.binder.MeterBinder; +import lombok.RequiredArgsConstructor; + +import java.util.Collections; +import java.util.Map; + +/** + * druid Metrics + * + * @author L.cm + */ +@RequiredArgsConstructor +public class DruidMetrics implements MeterBinder { + /** + * Prefix used for all Druid metric names. + */ + public static final String DRUID_METRIC_NAME_PREFIX = "druid"; + + private static final String METRIC_CATEGORY = "name"; + private static final String METRIC_NAME_CONNECT_MAX_TIME = DRUID_METRIC_NAME_PREFIX + ".connections.connect.max.time"; + private static final String METRIC_NAME_ALIVE_MAX_TIME = DRUID_METRIC_NAME_PREFIX + ".connections.alive.max.time"; + private static final String METRIC_NAME_ALIVE_MIN_TIME = DRUID_METRIC_NAME_PREFIX + ".connections.alive.min.time"; + + private static final String METRIC_NAME_CONNECT_COUNT = DRUID_METRIC_NAME_PREFIX + ".connections.connect.count"; + private static final String METRIC_NAME_ACTIVE_COUNT = DRUID_METRIC_NAME_PREFIX + ".connections.active.count"; + private static final String METRIC_NAME_CLOSE_COUNT = DRUID_METRIC_NAME_PREFIX + ".connections.close.count"; + private static final String METRIC_NAME_ERROR_COUNT = DRUID_METRIC_NAME_PREFIX + ".connections.error.count"; + private static final String METRIC_NAME_CONNECT_ERROR_COUNT = DRUID_METRIC_NAME_PREFIX + ".connections.connect.error.count"; + private static final String METRIC_NAME_COMMIT_COUNT = DRUID_METRIC_NAME_PREFIX + ".connections.commit.count"; + private static final String METRIC_NAME_ROLLBACK_COUNT = DRUID_METRIC_NAME_PREFIX + ".connections.rollback.count"; + + private final Map druidDataSourceMap; + private final Iterable tags; + + public DruidMetrics(Map druidDataSourceMap) { + this(druidDataSourceMap, Collections.emptyList()); + } + + @Override + public void bindTo(MeterRegistry meterRegistry) { + druidDataSourceMap.forEach((name, dataSource) -> { + JdbcDataSourceStat dsStats = dataSource.getDataSourceStat(); + JdbcConnectionStat connectionStat = dsStats.getConnectionStat(); + // time + Gauge.builder(METRIC_NAME_CONNECT_MAX_TIME, connectionStat, JdbcConnectionStat::getConnectMillisMax) + .description("Connection connect max time") + .tags(tags) + .tag(METRIC_CATEGORY, name) + .baseUnit(BaseUnits.MILLISECONDS) + .register(meterRegistry); + Gauge.builder(METRIC_NAME_ALIVE_MAX_TIME, connectionStat, JdbcConnectionStat::getAliveMillisMax) + .description("Connection alive max time") + .tags(tags) + .tag(METRIC_CATEGORY, name) + .baseUnit(BaseUnits.MILLISECONDS) + .register(meterRegistry); + Gauge.builder(METRIC_NAME_ALIVE_MIN_TIME, connectionStat, JdbcConnectionStat::getAliveMillisMin) + .description("Connection alive min time") + .tags(tags) + .tag(METRIC_CATEGORY, name) + .baseUnit(BaseUnits.MILLISECONDS) + .register(meterRegistry); + // count + Gauge.builder(METRIC_NAME_ACTIVE_COUNT, connectionStat, JdbcConnectionStat::getActiveCount) + .description("Connection active count") + .tags(tags) + .tag(METRIC_CATEGORY, name) + .register(meterRegistry); + Gauge.builder(METRIC_NAME_CONNECT_COUNT, connectionStat, JdbcConnectionStat::getConnectCount) + .description("Connection connect count") + .tags(tags) + .tag(METRIC_CATEGORY, name) + .register(meterRegistry); + Gauge.builder(METRIC_NAME_CLOSE_COUNT, connectionStat, JdbcConnectionStat::getCloseCount) + .description("Connection close count") + .tags(tags) + .tag(METRIC_CATEGORY, name) + .register(meterRegistry); + Gauge.builder(METRIC_NAME_ERROR_COUNT, connectionStat, JdbcConnectionStat::getErrorCount) + .description("Connection error count") + .tags(tags) + .tag(METRIC_CATEGORY, name) + .register(meterRegistry); + Gauge.builder(METRIC_NAME_CONNECT_ERROR_COUNT, connectionStat, JdbcConnectionStat::getConnectErrorCount) + .description("Connection connect error count") + .tags(tags) + .tag(METRIC_CATEGORY, name) + .register(meterRegistry); + Gauge.builder(METRIC_NAME_COMMIT_COUNT, connectionStat, JdbcConnectionStat::getCommitCount) + .description("Connecting commit count") + .tags(tags) + .tag(METRIC_CATEGORY, name) + .register(meterRegistry); + Gauge.builder(METRIC_NAME_ROLLBACK_COUNT, connectionStat, JdbcConnectionStat::getRollbackCount) + .description("Connection rollback count") + .tags(tags) + .tag(METRIC_CATEGORY, name) + .register(meterRegistry); + }); + } +} diff --git a/blade-starter-metrics/src/main/java/org/springblade/core/metrics/druid/DruidMetricsConfiguration.java b/blade-starter-metrics/src/main/java/org/springblade/core/metrics/druid/DruidMetricsConfiguration.java new file mode 100644 index 0000000..962cf81 --- /dev/null +++ b/blade-starter-metrics/src/main/java/org/springblade/core/metrics/druid/DruidMetricsConfiguration.java @@ -0,0 +1,107 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.metrics.druid; + +import com.alibaba.druid.filter.stat.StatFilter; +import com.alibaba.druid.pool.DruidDataSource; +import io.micrometer.core.instrument.MeterRegistry; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.jdbc.DataSourceUnwrapper; +import org.springframework.boot.jdbc.metadata.DataSourcePoolMetadataProvider; +import org.springframework.context.annotation.Bean; +import org.springframework.util.StringUtils; + +import javax.sql.DataSource; +import java.util.HashMap; +import java.util.Map; + +/** + * DruidDataSourceMetadata Provide + * + * @author L.cm + */ +@AutoConfiguration( + after = {MetricsAutoConfiguration.class, DataSourceAutoConfiguration.class, SimpleMetricsExportAutoConfiguration.class} +) +@ConditionalOnClass({DruidDataSource.class, MeterRegistry.class}) +@ConditionalOnBean({DataSource.class, MeterRegistry.class}) +public class DruidMetricsConfiguration { + private static final String DATASOURCE_SUFFIX = "dataSource"; + + @Bean + public DataSourcePoolMetadataProvider druidDataSourceMetadataProvider() { + return (dataSource) -> { + DruidDataSource druidDataSource = DataSourceUnwrapper.unwrap(dataSource, DruidDataSource.class); + if (druidDataSource != null) { + return new DruidDataSourcePoolMetadata(druidDataSource); + } + return null; + }; + } + + @Bean + @ConditionalOnMissingBean + public StatFilter statFilter() { + return new StatFilter(); + } + + @Bean + public DruidMetrics druidMetrics(ObjectProvider> dataSourcesProvider) { + Map dataSourceMap = dataSourcesProvider.getIfAvailable(HashMap::new); + Map druidDataSourceMap = new HashMap<>(2); + dataSourceMap.forEach((name, dataSource) -> { + // 保证连接池数据和 DataSourcePoolMetadataProvider 的一致 + DruidDataSource druidDataSource = DataSourceUnwrapper.unwrap(dataSource, DruidDataSource.class); + if (druidDataSource != null) { + druidDataSourceMap.put(getDataSourceName(name), druidDataSource); + } + }); + return druidDataSourceMap.isEmpty() ? null : new DruidMetrics(druidDataSourceMap); + } + + /** + * Get the name of a DataSource based on its {@code beanName}. + * + * @param beanName the name of the data source bean + * @return a name for the given data source + */ + private static String getDataSourceName(String beanName) { + if (beanName.length() > DATASOURCE_SUFFIX.length() + && StringUtils.endsWithIgnoreCase(beanName, DATASOURCE_SUFFIX)) { + return beanName.substring(0, beanName.length() - DATASOURCE_SUFFIX.length()); + } + return beanName; + } + +} diff --git a/blade-starter-metrics/src/main/java/org/springblade/core/metrics/sentinel/SentinelMetricsExtension.java b/blade-starter-metrics/src/main/java/org/springblade/core/metrics/sentinel/SentinelMetricsExtension.java new file mode 100644 index 0000000..d9da658 --- /dev/null +++ b/blade-starter-metrics/src/main/java/org/springblade/core/metrics/sentinel/SentinelMetricsExtension.java @@ -0,0 +1,89 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.metrics.sentinel; + +import com.alibaba.csp.sentinel.metric.extension.MetricExtension; +import com.alibaba.csp.sentinel.slots.block.BlockException; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Tags; +import org.springblade.core.auto.service.AutoService; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +/** + * Sentinel Metrics Extension + * + * @author L.cm + */ +@AutoService(MetricExtension.class) +public class SentinelMetricsExtension implements MetricExtension { + public static final String PASS_REQUESTS_TOTAL = "sentinel_pass_requests_total"; + public static final String BLOCK_REQUESTS_TOTAL = "sentinel_block_requests_total"; + public static final String SUCCESS_REQUESTS_TOTAL = "sentinel_success_requests_total"; + public static final String EXCEPTION_REQUESTS_TOTAL = "sentinel_exception_requests_total"; + public static final String REQUESTS_LATENCY_SECONDS = "sentinel_requests_latency_seconds"; + public static final String CURRENT_THREADS = "sentinel_current_threads"; + public static final String DEFAULT_TAT_NAME = "resource"; + private final AtomicLong CURRENT_THREAD_COUNT = new AtomicLong(0); + + @Override + public void addPass(String resource, int n, Object... args) { + Metrics.counter(PASS_REQUESTS_TOTAL, DEFAULT_TAT_NAME, resource).increment(n); + } + + @Override + public void addBlock(String resource, int n, String origin, BlockException ex, Object... args) { + Metrics.counter(BLOCK_REQUESTS_TOTAL, resource, ex.getClass().getSimpleName(), ex.getRuleLimitApp(), origin).increment(n); + } + + @Override + public void addSuccess(String resource, int n, Object... args) { + Metrics.counter(SUCCESS_REQUESTS_TOTAL, DEFAULT_TAT_NAME, resource).increment(n); + } + + @Override + public void addException(String resource, int n, Throwable throwable) { + Metrics.counter(EXCEPTION_REQUESTS_TOTAL, DEFAULT_TAT_NAME, resource).increment(n); + } + + @Override + public void addRt(String resource, long rt, Object... args) { + Metrics.timer(REQUESTS_LATENCY_SECONDS, DEFAULT_TAT_NAME, resource).record(rt, TimeUnit.MICROSECONDS); + } + + @Override + public void increaseThreadNum(String resource, Object... args) { + Tags tags = Tags.of(DEFAULT_TAT_NAME, resource); + Metrics.gauge(CURRENT_THREADS, tags, CURRENT_THREAD_COUNT, AtomicLong::incrementAndGet); + } + + @Override + public void decreaseThreadNum(String resource, Object... args) { + Tags tags = Tags.of(DEFAULT_TAT_NAME, resource); + Metrics.gauge(CURRENT_THREADS, tags, CURRENT_THREAD_COUNT, AtomicLong::decrementAndGet); + } +} diff --git a/blade-starter-metrics/src/main/java/org/springblade/core/metrics/undertow/UndertowMetrics.java b/blade-starter-metrics/src/main/java/org/springblade/core/metrics/undertow/UndertowMetrics.java new file mode 100644 index 0000000..e076de1 --- /dev/null +++ b/blade-starter-metrics/src/main/java/org/springblade/core/metrics/undertow/UndertowMetrics.java @@ -0,0 +1,273 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.metrics.undertow; + +import io.micrometer.core.instrument.*; +import io.micrometer.core.instrument.binder.BaseUnits; +import io.undertow.Undertow; +import io.undertow.server.ConnectorStatistics; +import io.undertow.server.session.SessionManagerStatistics; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.context.event.ApplicationStartedEvent; +import org.springframework.boot.web.embedded.undertow.UndertowServletWebServer; +import org.springframework.boot.web.embedded.undertow.UndertowWebServer; +import org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContext; +import org.springframework.boot.web.server.WebServer; +import org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext; +import org.springframework.context.ApplicationListener; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.util.ReflectionUtils; +import org.xnio.management.XnioWorkerMXBean; + +import java.lang.reflect.Field; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +/** + * Undertow Metrics + * + * @author L.cm + */ +@RequiredArgsConstructor +public class UndertowMetrics implements ApplicationListener { + /** + * Prefix used for all Undertow metric names. + */ + public static final String UNDERTOW_METRIC_NAME_PREFIX = "undertow"; + /** + * XWorker + */ + private static final String METRIC_NAME_X_WORK_WORKER_POOL_CORE_SIZE = UNDERTOW_METRIC_NAME_PREFIX + ".xwork.worker.pool.core.size"; + private static final String METRIC_NAME_X_WORK_WORKER_POOL_MAX_SIZE = UNDERTOW_METRIC_NAME_PREFIX + ".xwork.worker.pool.max.size"; + private static final String METRIC_NAME_X_WORK_WORKER_POOL_SIZE = UNDERTOW_METRIC_NAME_PREFIX + ".xwork.worker.pool.size"; + private static final String METRIC_NAME_X_WORK_WORKER_THREAD_BUSY_COUNT = UNDERTOW_METRIC_NAME_PREFIX + ".xwork.worker.thread.busy.count"; + private static final String METRIC_NAME_X_WORK_IO_THREAD_COUNT = UNDERTOW_METRIC_NAME_PREFIX + ".xwork.io.thread.count"; + private static final String METRIC_NAME_X_WORK_WORKER_QUEUE_SIZE = UNDERTOW_METRIC_NAME_PREFIX + ".xwork.worker.queue.size"; + /** + * connectors + */ + private static final String METRIC_NAME_CONNECTORS_REQUESTS_COUNT = UNDERTOW_METRIC_NAME_PREFIX + ".connectors.requests.count"; + private static final String METRIC_NAME_CONNECTORS_REQUESTS_ERROR_COUNT = UNDERTOW_METRIC_NAME_PREFIX + ".connectors.requests.error.count"; + private static final String METRIC_NAME_CONNECTORS_REQUESTS_ACTIVE = UNDERTOW_METRIC_NAME_PREFIX + ".connectors.requests.active"; + private static final String METRIC_NAME_CONNECTORS_REQUESTS_ACTIVE_MAX = UNDERTOW_METRIC_NAME_PREFIX + ".connectors.requests.active.max"; + private static final String METRIC_NAME_CONNECTORS_BYTES_SENT = UNDERTOW_METRIC_NAME_PREFIX + ".connectors.bytes.sent"; + private static final String METRIC_NAME_CONNECTORS_BYTES_RECEIVED = UNDERTOW_METRIC_NAME_PREFIX + ".connectors.bytes.received"; + private static final String METRIC_NAME_CONNECTORS_PROCESSING_TIME = UNDERTOW_METRIC_NAME_PREFIX + ".connectors.processing.time"; + private static final String METRIC_NAME_CONNECTORS_PROCESSING_TIME_MAX = UNDERTOW_METRIC_NAME_PREFIX + ".connectors.processing.time.max"; + private static final String METRIC_NAME_CONNECTORS_CONNECTIONS_ACTIVE = UNDERTOW_METRIC_NAME_PREFIX + ".connectors.connections.active"; + private static final String METRIC_NAME_CONNECTORS_CONNECTIONS_ACTIVE_MAX = UNDERTOW_METRIC_NAME_PREFIX + ".connectors.connections.active.max"; + /** + * session + */ + private static final String METRIC_NAME_SESSIONS_ACTIVE_MAX = UNDERTOW_METRIC_NAME_PREFIX + ".sessions.active.max"; + private static final String METRIC_NAME_SESSIONS_ACTIVE_CURRENT = UNDERTOW_METRIC_NAME_PREFIX + ".sessions.active.current"; + private static final String METRIC_NAME_SESSIONS_CREATED = UNDERTOW_METRIC_NAME_PREFIX + ".sessions.created"; + private static final String METRIC_NAME_SESSIONS_EXPIRED = UNDERTOW_METRIC_NAME_PREFIX + ".sessions.expired"; + private static final String METRIC_NAME_SESSIONS_REJECTED = UNDERTOW_METRIC_NAME_PREFIX + ".sessions.rejected"; + private static final String METRIC_NAME_SESSIONS_ALIVE_MAX = UNDERTOW_METRIC_NAME_PREFIX + ".sessions.alive.max"; + + private static final Field UNDERTOW_FIELD; + private final Iterable tags; + + public UndertowMetrics() { + this.tags = Collections.emptyList(); + } + + @Override + public void onApplicationEvent(ApplicationStartedEvent event) { + ConfigurableApplicationContext applicationContext = event.getApplicationContext(); + // find UndertowWebServer + UndertowWebServer undertowWebServer = findUndertowWebServer(applicationContext); + if (undertowWebServer == null) { + return; + } + Undertow undertow = getUndertow(undertowWebServer); + XnioWorkerMXBean xWorker = undertow.getWorker().getMXBean(); + MeterRegistry registry = applicationContext.getBean(MeterRegistry.class); + // xWorker 指标 + registerXWorker(registry, xWorker); + // 连接信息指标 + List listenerInfoList = undertow.getListenerInfo(); + listenerInfoList.forEach(listenerInfo -> registerConnectorStatistics(registry, listenerInfo)); + // 如果是 web 监控,添加 session 指标 + if (undertowWebServer instanceof UndertowServletWebServer) { + SessionManagerStatistics statistics = ((UndertowServletWebServer) undertowWebServer).getDeploymentManager() + .getDeployment() + .getSessionManager() + .getStatistics(); + registerSessionStatistics(registry, statistics); + } + } + + private void registerXWorker(MeterRegistry registry, XnioWorkerMXBean workerMXBean) { + Gauge.builder(METRIC_NAME_X_WORK_WORKER_POOL_CORE_SIZE, workerMXBean, XnioWorkerMXBean::getCoreWorkerPoolSize) + .description("XWork core worker pool size") + .tags(tags) + .tag("name", workerMXBean.getName()) + .register(registry); + Gauge.builder(METRIC_NAME_X_WORK_WORKER_POOL_MAX_SIZE, workerMXBean, XnioWorkerMXBean::getMaxWorkerPoolSize) + .description("XWork max worker pool size") + .tags(tags) + .tag("name", workerMXBean.getName()) + .register(registry); + Gauge.builder(METRIC_NAME_X_WORK_WORKER_POOL_SIZE, workerMXBean, XnioWorkerMXBean::getWorkerPoolSize) + .description("XWork worker pool size") + .tags(tags) + .tag("name", workerMXBean.getName()) + .register(registry); + Gauge.builder(METRIC_NAME_X_WORK_WORKER_THREAD_BUSY_COUNT, workerMXBean, XnioWorkerMXBean::getBusyWorkerThreadCount) + .description("XWork busy worker thread count") + .tags(tags) + .tag("name", workerMXBean.getName()) + .register(registry); + Gauge.builder(METRIC_NAME_X_WORK_IO_THREAD_COUNT, workerMXBean, XnioWorkerMXBean::getIoThreadCount) + .description("XWork io thread count") + .tags(tags) + .tag("name", workerMXBean.getName()) + .register(registry); + Gauge.builder(METRIC_NAME_X_WORK_WORKER_QUEUE_SIZE, workerMXBean, XnioWorkerMXBean::getWorkerQueueSize) + .description("XWork worker queue size") + .tags(tags) + .tag("name", workerMXBean.getName()) + .register(registry); + } + + private void registerConnectorStatistics(MeterRegistry registry, Undertow.ListenerInfo listenerInfo) { + String protocol = listenerInfo.getProtcol(); + ConnectorStatistics statistics = listenerInfo.getConnectorStatistics(); + Gauge.builder(METRIC_NAME_CONNECTORS_REQUESTS_COUNT, statistics, ConnectorStatistics::getRequestCount) + .tags(tags) + .tag("protocol", protocol) + .register(registry); + Gauge.builder(METRIC_NAME_CONNECTORS_REQUESTS_ERROR_COUNT, statistics, ConnectorStatistics::getErrorCount) + .tags(tags) + .tag("protocol", protocol) + .register(registry); + Gauge.builder(METRIC_NAME_CONNECTORS_REQUESTS_ACTIVE, statistics, ConnectorStatistics::getActiveRequests) + .tags(tags) + .tag("protocol", protocol) + .baseUnit(BaseUnits.CONNECTIONS) + .register(registry); + Gauge.builder(METRIC_NAME_CONNECTORS_REQUESTS_ACTIVE_MAX, statistics, ConnectorStatistics::getMaxActiveRequests) + .tags(tags) + .tag("protocol", protocol) + .baseUnit(BaseUnits.CONNECTIONS) + .register(registry); + + Gauge.builder(METRIC_NAME_CONNECTORS_BYTES_SENT, statistics, ConnectorStatistics::getBytesSent) + .tags(tags) + .tag("protocol", protocol) + .baseUnit(BaseUnits.BYTES) + .register(registry); + Gauge.builder(METRIC_NAME_CONNECTORS_BYTES_RECEIVED, statistics, ConnectorStatistics::getBytesReceived) + .tags(tags) + .tag("protocol", protocol) + .baseUnit(BaseUnits.BYTES) + .register(registry); + + Gauge.builder(METRIC_NAME_CONNECTORS_PROCESSING_TIME, statistics, (s) -> TimeUnit.NANOSECONDS.toMillis(s.getProcessingTime())) + .tags(tags) + .tag("protocol", protocol) + .baseUnit(BaseUnits.MILLISECONDS) + .register(registry); + Gauge.builder(METRIC_NAME_CONNECTORS_PROCESSING_TIME_MAX, statistics, (s) -> TimeUnit.NANOSECONDS.toMillis(s.getMaxProcessingTime())) + .tags(tags) + .tag("protocol", protocol) + .baseUnit(BaseUnits.MILLISECONDS) + .register(registry); + + Gauge.builder(METRIC_NAME_CONNECTORS_CONNECTIONS_ACTIVE, statistics, ConnectorStatistics::getActiveConnections) + .tags(tags) + .tag("protocol", protocol) + .baseUnit(BaseUnits.CONNECTIONS) + .register(registry); + Gauge.builder(METRIC_NAME_CONNECTORS_CONNECTIONS_ACTIVE_MAX, statistics, ConnectorStatistics::getMaxActiveConnections) + .tags(tags) + .tag("protocol", protocol) + .baseUnit(BaseUnits.CONNECTIONS) + .register(registry); + } + + private void registerSessionStatistics(MeterRegistry registry, SessionManagerStatistics statistics) { + Gauge.builder(METRIC_NAME_SESSIONS_ACTIVE_MAX, statistics, SessionManagerStatistics::getMaxActiveSessions) + .tags(tags) + .baseUnit(BaseUnits.SESSIONS) + .register(registry); + + Gauge.builder(METRIC_NAME_SESSIONS_ACTIVE_CURRENT, statistics, SessionManagerStatistics::getActiveSessionCount) + .tags(tags) + .baseUnit(BaseUnits.SESSIONS) + .register(registry); + + FunctionCounter.builder(METRIC_NAME_SESSIONS_CREATED, statistics, SessionManagerStatistics::getCreatedSessionCount) + .tags(tags) + .baseUnit(BaseUnits.SESSIONS) + .register(registry); + + FunctionCounter.builder(METRIC_NAME_SESSIONS_EXPIRED, statistics, SessionManagerStatistics::getExpiredSessionCount) + .tags(tags) + .baseUnit(BaseUnits.SESSIONS) + .register(registry); + + FunctionCounter.builder(METRIC_NAME_SESSIONS_REJECTED, statistics, SessionManagerStatistics::getRejectedSessions) + .tags(tags) + .baseUnit(BaseUnits.SESSIONS) + .register(registry); + + TimeGauge.builder(METRIC_NAME_SESSIONS_ALIVE_MAX, statistics, TimeUnit.SECONDS, SessionManagerStatistics::getHighestSessionCount) + .tags(tags) + .register(registry); + } + + static { + UNDERTOW_FIELD = ReflectionUtils.findField(UndertowWebServer.class, "undertow"); + Objects.requireNonNull(UNDERTOW_FIELD, "UndertowWebServer class field undertow not exist."); + ReflectionUtils.makeAccessible(UNDERTOW_FIELD); + } + + private static Undertow getUndertow(UndertowWebServer undertowWebServer) { + return (Undertow) ReflectionUtils.getField(UNDERTOW_FIELD, undertowWebServer); + } + + private static UndertowWebServer findUndertowWebServer(ConfigurableApplicationContext applicationContext) { + WebServer webServer; + if (applicationContext instanceof ReactiveWebServerApplicationContext) { + webServer = ((ReactiveWebServerApplicationContext) applicationContext).getWebServer(); + } else if (applicationContext instanceof ServletWebServerApplicationContext) { + webServer = ((ServletWebServerApplicationContext) applicationContext).getWebServer(); + } else { + return null; + } + if (webServer instanceof UndertowWebServer) { + return (UndertowWebServer) webServer; + } + return null; + } + +} diff --git a/blade-starter-metrics/src/main/java/org/springblade/core/metrics/undertow/UndertowMetricsConfiguration.java b/blade-starter-metrics/src/main/java/org/springblade/core/metrics/undertow/UndertowMetricsConfiguration.java new file mode 100644 index 0000000..1adcaed --- /dev/null +++ b/blade-starter-metrics/src/main/java/org/springblade/core/metrics/undertow/UndertowMetricsConfiguration.java @@ -0,0 +1,57 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.metrics.undertow; + +import io.undertow.Undertow; +import io.undertow.UndertowOptions; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration; +import org.springframework.boot.web.embedded.undertow.UndertowBuilderCustomizer; +import org.springframework.context.annotation.Bean; + + +/** + * Undertow Metrics 配置 + * + * @author L.cm + */ +@AutoConfiguration(before = ServletWebServerFactoryAutoConfiguration.class) +@ConditionalOnClass(Undertow.class) +public class UndertowMetricsConfiguration { + + @Bean + public UndertowMetrics undertowMetrics() { + return new UndertowMetrics(); + } + + @Bean + public UndertowBuilderCustomizer undertowBuilderCustomizerEnableStatistics() { + return builder -> builder.setServerOption(UndertowOptions.ENABLE_STATISTICS, true); + } + +} diff --git a/blade-starter-mongo/pom.xml b/blade-starter-mongo/pom.xml new file mode 100644 index 0000000..5f77998 --- /dev/null +++ b/blade-starter-mongo/pom.xml @@ -0,0 +1,37 @@ + + + 4.0.0 + + BladeX-Tool + org.springblade + ${revision} + + + blade-starter-mongo + ${project.artifactId} + ${project.parent.version} + jar + + + + org.springblade + blade-core-tool + + + org.springframework.boot + spring-boot-starter-data-mongodb + + + org.springframework.boot + spring-boot-starter-json + + + org.springblade + blade-core-auto + provided + + + + diff --git a/blade-starter-mongo/src/main/java/org/springblade/core/mongo/config/MongoConfiguration.java b/blade-starter-mongo/src/main/java/org/springblade/core/mongo/config/MongoConfiguration.java new file mode 100644 index 0000000..e578acb --- /dev/null +++ b/blade-starter-mongo/src/main/java/org/springblade/core/mongo/config/MongoConfiguration.java @@ -0,0 +1,28 @@ +package org.springblade.core.mongo.config; + +import org.springblade.core.mongo.converter.DBObjectToJsonNodeConverter; +import org.springblade.core.mongo.converter.JsonNodeToDocumentConverter; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.mongodb.core.convert.MongoCustomConversions; + +import java.util.ArrayList; +import java.util.List; + +/** + * mongo 配置 + * + * @author L.cm + */ +@AutoConfiguration +public class MongoConfiguration { + + @Bean + public MongoCustomConversions customConversions() { + List> converters = new ArrayList<>(2); + converters.add(DBObjectToJsonNodeConverter.INSTANCE); + converters.add(JsonNodeToDocumentConverter.INSTANCE); + return new MongoCustomConversions(converters); + } +} diff --git a/blade-starter-mongo/src/main/java/org/springblade/core/mongo/converter/DBObjectToJsonNodeConverter.java b/blade-starter-mongo/src/main/java/org/springblade/core/mongo/converter/DBObjectToJsonNodeConverter.java new file mode 100644 index 0000000..da97877 --- /dev/null +++ b/blade-starter-mongo/src/main/java/org/springblade/core/mongo/converter/DBObjectToJsonNodeConverter.java @@ -0,0 +1,30 @@ +package org.springblade.core.mongo.converter; + +import com.fasterxml.jackson.databind.JsonNode; +import org.bson.BasicBSONObject; +import org.springblade.core.tool.jackson.JsonUtil; +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.convert.ReadingConverter; +import org.springframework.lang.Nullable; + +/** + * mongo DBObject 转 jsonNode + * + * @author L.cm + */ +@ReadingConverter +public enum DBObjectToJsonNodeConverter implements Converter { + /** + * 实例 + */ + INSTANCE; + + @Override + public JsonNode convert(@Nullable BasicBSONObject source) { + if (source == null) { + return null; + } + return JsonUtil.getInstance().valueToTree(source); + } +} + diff --git a/blade-starter-mongo/src/main/java/org/springblade/core/mongo/converter/JsonNodeToDocumentConverter.java b/blade-starter-mongo/src/main/java/org/springblade/core/mongo/converter/JsonNodeToDocumentConverter.java new file mode 100644 index 0000000..5ec3067 --- /dev/null +++ b/blade-starter-mongo/src/main/java/org/springblade/core/mongo/converter/JsonNodeToDocumentConverter.java @@ -0,0 +1,25 @@ +package org.springblade.core.mongo.converter; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.bson.Document; +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.convert.WritingConverter; +import org.springframework.lang.Nullable; + +/** + * JsonNode 转 mongo Document + * + * @author L.cm + */ +@WritingConverter +public enum JsonNodeToDocumentConverter implements Converter { + /** + * 实例 + */ + INSTANCE; + + @Override + public Document convert(@Nullable ObjectNode source) { + return source == null ? null : Document.parse(source.toString()); + } +} diff --git a/blade-starter-mongo/src/main/java/org/springblade/core/mongo/utils/JsonNodeInfo.java b/blade-starter-mongo/src/main/java/org/springblade/core/mongo/utils/JsonNodeInfo.java new file mode 100644 index 0000000..7d4ae04 --- /dev/null +++ b/blade-starter-mongo/src/main/java/org/springblade/core/mongo/utils/JsonNodeInfo.java @@ -0,0 +1,85 @@ +package org.springblade.core.mongo.utils; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Getter; +import org.springframework.util.Assert; + +import java.util.LinkedList; +import java.util.StringJoiner; + +/** + * json tree 节点信息 + * + * @author L.cm + */ +public class JsonNodeInfo { + /** + * mongo keys: class1.class2.item + */ + private volatile String nodeKeys; + /** + * jsonPath语法:/class1/class2/item + */ + private volatile String nodePath; + /** + * 节点关系 + */ + @Getter + private final LinkedList elements; + /** + * tree 的 叶子节点,此处为引用 + */ + @Getter + private final JsonNode leafNode; + + public JsonNodeInfo(LinkedList elements, JsonNode leafNode) { + Assert.notNull(elements, "elements can not be null."); + this.nodeKeys = null; + this.nodePath = null; + this.elements = elements; + this.leafNode = leafNode; + } + + /** + * 获取 mongo db的 key 语法 + * @return mongo db的 key 语法 + */ + public String getNodeKeys() { + if (nodeKeys == null) { + synchronized (this) { + if (nodeKeys == null) { + StringJoiner nodeKeysJoiner = new StringJoiner("."); + elements.forEach(nodeKeysJoiner::add); + nodeKeys = nodeKeysJoiner.toString(); + } + } + } + return nodeKeys; + } + + /** + * 获取 json path 语法路径 + * @return jsonPath 路径 + */ + public String getNodePath() { + if (nodePath == null) { + synchronized (this) { + if (nodePath == null) { + StringJoiner nodePathJoiner = new StringJoiner("/", "/", ""); + elements.forEach(nodePathJoiner::add); + nodePath = nodePathJoiner.toString(); + } + } + } + return nodePath; + } + + /** + * 获取第一个元素 + * @return element + */ + public String getFirst() { + return elements.getFirst(); + } + +} diff --git a/blade-starter-mongo/src/main/java/org/springblade/core/mongo/utils/MongoJsonUtils.java b/blade-starter-mongo/src/main/java/org/springblade/core/mongo/utils/MongoJsonUtils.java new file mode 100644 index 0000000..31acdab --- /dev/null +++ b/blade-starter-mongo/src/main/java/org/springblade/core/mongo/utils/MongoJsonUtils.java @@ -0,0 +1,126 @@ +package org.springblade.core.mongo.utils; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import java.util.*; + +/** + * 处理 mongo json 数据结构 + * + * @author L.cm + */ +public class MongoJsonUtils { + + /** + * 获取所有的叶子节点和路径信息 + * + * @param jsonNode jsonTree + * @return tree叶子信息 + */ + public static List getLeafNodes(JsonNode jsonNode) { + if (jsonNode == null || !jsonNode.isObject()) { + return Collections.emptyList(); + } + List list = new ArrayList<>(); + // 双向的队列 Deque 代替 Stack,Stack 性能不好 + LinkedList deque = new LinkedList<>(); + // 递归获取叶子 🍃🍃🍃 节点 + getLeafNodes(jsonNode, null, deque, list); + return list; + } + + private static void getLeafNodes(JsonNode jsonNode, JsonNode parentNode, LinkedList deque, List list) { + Iterator> iterator; + if (parentNode == null) { + iterator = jsonNode.fields(); + } else { + iterator = parentNode.fields(); + } + // tree 子节点 + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + String fieldName = entry.getKey(); + JsonNode nextNode = entry.getValue(); + // 如果不是值节点 + if (nextNode.isObject()) { + // 添加到队列尾,先进先出 + deque.addLast(fieldName); + getLeafNodes(parentNode, nextNode, deque, list); + } + // 如果是值节点,也就是到叶子节点了,取叶子节点上级即可 + if (nextNode.isValueNode()) { + // 封装节点列表 + LinkedList elements = new LinkedList<>(deque); + // tree 的 叶子节点,此处为引用 + list.add(new JsonNodeInfo(elements, parentNode)); + break; + } + // 栈非空时弹出 + if (!deque.isEmpty()) { + deque.removeLast(); + } + } + } + + /** + * 构建树形节点 + * + * @param jsonNode 父级节点 + * @param elements tree节点列表 + * @return JsonNode 叶子节点,返回用于塞数据 + */ + public static ObjectNode buildNode(ObjectNode jsonNode, List elements) { + ObjectNode newNode = jsonNode; + for (String element : elements) { + // 如果已经存在节点,这不生成新的 + if (newNode.has(element)) { + newNode = (ObjectNode) newNode.get(element); + } else { + newNode = newNode.putObject(element); + } + } + return newNode; + } + + /** + * 获取所有 🍃🍃🍃 节点的值,并构建成 mongodb update 语句 + * @param prefix 前缀 + * @param nodeKeys mongo keys + * @param objectNode tree 🍃 节点 + * @return tree 节点信息 + */ + public static Map getAllUpdate(String prefix, String nodeKeys, ObjectNode objectNode) { + Map values = new HashMap<>(8); + Iterator iterator = objectNode.fieldNames(); + while (iterator.hasNext()) { + String fieldName = iterator.next(); + JsonNode valueNode = objectNode.get(fieldName); + if (valueNode.isValueNode()) { + Object value; + if (valueNode.isShort()) { + value = valueNode.shortValue(); + } else if (valueNode.isInt()) { + value = valueNode.intValue(); + } else if (valueNode.isLong()) { + value = valueNode.longValue(); + } else if (valueNode.isBoolean()) { + value = valueNode.booleanValue(); + } else if (valueNode.isFloat()) { + value = valueNode.floatValue(); + } else if (valueNode.isDouble()) { + value = valueNode.doubleValue(); + } else if (valueNode.isMissingNode()) { + value = null; + } else { + value = valueNode.textValue(); + } + if (value != null) { + String valueKey = prefix + '.' + nodeKeys + '.' + fieldName; + values.put(valueKey, value); + } + } + } + return values; + } +} diff --git a/blade-starter-mybatis/pom.xml b/blade-starter-mybatis/pom.xml new file mode 100644 index 0000000..b26e056 --- /dev/null +++ b/blade-starter-mybatis/pom.xml @@ -0,0 +1,64 @@ + + + 4.0.0 + + BladeX-Tool + org.springblade + ${revision} + + + blade-starter-mybatis + ${project.artifactId} + ${project.parent.version} + jar + + + + + com.baomidou + mybatis-plus + + + org.mybatis + mybatis + + + org.mybatis + mybatis-spring + + + org.mybatis + mybatis-typehandlers-jsr310 + + + + org.springframework.boot + spring-boot-starter-jdbc + + + tomcat-jdbc + org.apache.tomcat + + + + + + com.alibaba + druid + + + + org.springblade + blade-starter-auth + + + + org.springblade + blade-core-auto + provided + + + + diff --git a/blade-starter-mybatis/src/main/java/org/springblade/core/mp/BladeMetaObjectHandler.java b/blade-starter-mybatis/src/main/java/org/springblade/core/mp/BladeMetaObjectHandler.java new file mode 100644 index 0000000..b40045b --- /dev/null +++ b/blade-starter-mybatis/src/main/java/org/springblade/core/mp/BladeMetaObjectHandler.java @@ -0,0 +1,50 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.mp; + +import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; +import lombok.extern.slf4j.Slf4j; +import org.apache.ibatis.reflection.MetaObject; + +/** + * mybatisplus自定义填充 + * + * @author Chill + */ +@Slf4j +public class BladeMetaObjectHandler implements MetaObjectHandler { + + @Override + public void insertFill(MetaObject metaObject) { + + } + + @Override + public void updateFill(MetaObject metaObject) { + + } + +} diff --git a/blade-starter-mybatis/src/main/java/org/springblade/core/mp/base/BaseEntity.java b/blade-starter-mybatis/src/main/java/org/springblade/core/mp/base/BaseEntity.java new file mode 100644 index 0000000..cc20b65 --- /dev/null +++ b/blade-starter-mybatis/src/main/java/org/springblade/core/mp/base/BaseEntity.java @@ -0,0 +1,107 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.mp.base; + + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableLogic; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springblade.core.tool.utils.DateUtil; +import org.springframework.format.annotation.DateTimeFormat; + +import java.io.Serializable; +import java.util.Date; + +/** + * 基础实体类 + * + * @author Chill + */ +@Data +public class BaseEntity implements Serializable { + /** + * 主键id + */ + @JsonSerialize(using = ToStringSerializer.class) + @Schema(description = "主键id") + @TableId(value = "id", type = IdType.ASSIGN_ID) + private Long id; + + /** + * 创建人 + */ + @JsonSerialize(using = ToStringSerializer.class) + @Schema(description = "创建人") + private Long createUser; + + /** + * 创建部门 + */ + @JsonSerialize(using = ToStringSerializer.class) + @Schema(description = "创建部门") + private Long createDept; + + /** + * 创建时间 + */ + @DateTimeFormat(pattern = DateUtil.PATTERN_DATETIME) + @JsonFormat(pattern = DateUtil.PATTERN_DATETIME) + @Schema(description = "创建时间") + private Date createTime; + + /** + * 更新人 + */ + @JsonSerialize(using = ToStringSerializer.class) + @Schema(description = "更新人") + private Long updateUser; + + /** + * 更新时间 + */ + @DateTimeFormat(pattern = DateUtil.PATTERN_DATETIME) + @JsonFormat(pattern = DateUtil.PATTERN_DATETIME) + @Schema(description = "更新时间") + private Date updateTime; + + /** + * 状态[1:正常] + */ + @Schema(description = "业务状态") + private Integer status; + + /** + * 状态[0:未删除,1:删除] + */ + @TableLogic + @Schema(description = "是否已删除") + private Integer isDeleted; +} diff --git a/blade-starter-mybatis/src/main/java/org/springblade/core/mp/base/BaseService.java b/blade-starter-mybatis/src/main/java/org/springblade/core/mp/base/BaseService.java new file mode 100644 index 0000000..02a72dc --- /dev/null +++ b/blade-starter-mybatis/src/main/java/org/springblade/core/mp/base/BaseService.java @@ -0,0 +1,78 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.mp.base; + +import com.baomidou.mybatisplus.core.toolkit.support.SFunction; +import com.baomidou.mybatisplus.extension.service.IService; + +import jakarta.validation.constraints.NotEmpty; +import java.util.List; + +/** + * 基础业务接口 + * + * @param + * @author Chill + */ +public interface BaseService extends IService { + + /** + * 逻辑删除 + * + * @param ids id集合 + * @return + */ + boolean deleteLogic(@NotEmpty List ids); + + /** + * 变更状态 + * + * @param ids id集合 + * @param status 状态值 + * @return + */ + boolean changeStatus(@NotEmpty List ids, Integer status); + + /** + * 判断是否有字段重复 + * + * @param field 字段 + * @param value 值 + * @return boolean + */ + boolean isFieldDuplicate(SFunction field, Object value); + + /** + * 判断是否有字段重复 + * + * @param field 字段 + * @param value 值 + * @param excludedId 排除的id + * @return boolean + */ + boolean isFieldDuplicate(SFunction field, Object value, Long excludedId); + +} diff --git a/blade-starter-mybatis/src/main/java/org/springblade/core/mp/base/BaseServiceImpl.java b/blade-starter-mybatis/src/main/java/org/springblade/core/mp/base/BaseServiceImpl.java new file mode 100644 index 0000000..1b8860d --- /dev/null +++ b/blade-starter-mybatis/src/main/java/org/springblade/core/mp/base/BaseServiceImpl.java @@ -0,0 +1,183 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.mp.base; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.core.toolkit.support.SFunction; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.SneakyThrows; +import org.springblade.core.secure.BladeUser; +import org.springblade.core.secure.utils.AuthUtil; +import org.springblade.core.tool.constant.BladeConstant; +import org.springblade.core.tool.utils.*; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import jakarta.validation.constraints.NotEmpty; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.List; + +/** + * 业务封装基础类 + * + * @param mapper + * @param model + * @author Chill + */ +@Validated +public class BaseServiceImpl, T extends BaseEntity> extends ServiceImpl implements BaseService { + + @Override + public boolean save(T entity) { + this.resolveEntity(entity); + return super.save(entity); + } + + @Override + public boolean saveBatch(Collection entityList, int batchSize) { + entityList.forEach(this::resolveEntity); + return super.saveBatch(entityList, batchSize); + } + + @Override + public boolean updateById(T entity) { + this.resolveEntity(entity); + return super.updateById(entity); + } + + @Override + public boolean updateBatchById(Collection entityList, int batchSize) { + entityList.forEach(this::resolveEntity); + return super.updateBatchById(entityList, batchSize); + } + + @Override + public boolean saveOrUpdate(T entity) { + if (entity.getId() == null) { + return this.save(entity); + } else { + return this.updateById(entity); + } + } + + @Override + public boolean saveOrUpdateBatch(Collection entityList, int batchSize) { + entityList.forEach(this::resolveEntity); + return super.saveOrUpdateBatch(entityList, batchSize); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean deleteLogic(@NotEmpty List ids) { + BladeUser user = AuthUtil.getUser(); + List list = new ArrayList<>(); + ids.forEach(id -> { + T entity = BeanUtil.newInstance(currentModelClass()); + if (user != null) { + entity.setUpdateUser(user.getUserId()); + } + entity.setUpdateTime(DateUtil.now()); + entity.setId(id); + list.add(entity); + }); + return super.updateBatchById(list) && super.removeByIds(ids); + } + + @Override + public boolean changeStatus(@NotEmpty List ids, Integer status) { + BladeUser user = AuthUtil.getUser(); + List list = new ArrayList<>(); + ids.forEach(id -> { + T entity = BeanUtil.newInstance(currentModelClass()); + if (user != null) { + entity.setUpdateUser(user.getUserId()); + } + entity.setUpdateTime(DateUtil.now()); + entity.setId(id); + entity.setStatus(status); + list.add(entity); + }); + return super.updateBatchById(list); + } + + @Override + public boolean isFieldDuplicate(SFunction field, Object value) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(field, value); + return super.count(queryWrapper) > 0; + } + + @Override + public boolean isFieldDuplicate(SFunction field, Object value, Long excludedId) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(field, value); + if (excludedId != null) { + queryWrapper.ne(T::getId, excludedId); + } + return super.count(queryWrapper) > 0; + } + + @SneakyThrows + private void resolveEntity(T entity) { + BladeUser user = AuthUtil.getUser(); + Date now = DateUtil.now(); + if (entity.getId() == null) { + // 处理新增逻辑 + if (user != null) { + entity.setCreateUser(user.getUserId()); + entity.setCreateDept(Func.firstLong(user.getDeptId())); + entity.setUpdateUser(user.getUserId()); + } + if (entity.getStatus() == null) { + entity.setStatus(BladeConstant.DB_STATUS_NORMAL); + } + entity.setCreateTime(now); + } else if (user != null) { + // 处理修改逻辑 + entity.setUpdateUser(user.getUserId()); + } + // 处理通用逻辑 + entity.setUpdateTime(now); + entity.setIsDeleted(BladeConstant.DB_NOT_DELETED); + // 处理多租户逻辑,若字段值为空,则不进行操作 + Field field = ReflectUtil.getField(entity.getClass(), BladeConstant.DB_TENANT_KEY); + if (ObjectUtil.isNotEmpty(field)) { + Method getTenantId = ClassUtil.getMethod(entity.getClass(), BladeConstant.DB_TENANT_KEY_GET_METHOD); + String tenantId = String.valueOf(getTenantId.invoke(entity)); + if (ObjectUtil.isEmpty(tenantId)) { + Method setTenantId = ClassUtil.getMethod(entity.getClass(), BladeConstant.DB_TENANT_KEY_SET_METHOD, String.class); + setTenantId.invoke(entity, (Object) null); + } + } + } + +} diff --git a/blade-starter-mybatis/src/main/java/org/springblade/core/mp/base/BizEntity.java b/blade-starter-mybatis/src/main/java/org/springblade/core/mp/base/BizEntity.java new file mode 100644 index 0000000..93a072d --- /dev/null +++ b/blade-starter-mybatis/src/main/java/org/springblade/core/mp/base/BizEntity.java @@ -0,0 +1,113 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.mp.base; + + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableLogic; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springblade.core.tool.utils.DateUtil; +import org.springframework.format.annotation.DateTimeFormat; + +import java.io.Serializable; +import java.util.Date; + +/** + * 自定义业务实体类(推荐自行修改拓展并用于特定业务模块) + * + * @author Chill + */ +@Data +public class BizEntity implements Serializable { + /** + * 主键id + */ + @JsonSerialize(using = ToStringSerializer.class) + @Schema(description = "主键id") + @TableId(value = "id", type = IdType.ASSIGN_ID) + private Long id; + + /** + * 租户ID + */ + @Schema(description = "租户ID") + private String tenantId; + + /** + * 创建人 + */ + @JsonSerialize(using = ToStringSerializer.class) + @Schema(description = "创建人") + private Long createUser; + + /** + * 创建部门 + */ + @JsonSerialize(using = ToStringSerializer.class) + @Schema(description = "创建部门") + private Long createDept; + + /** + * 创建时间 + */ + @DateTimeFormat(pattern = DateUtil.PATTERN_DATETIME) + @JsonFormat(pattern = DateUtil.PATTERN_DATETIME) + @Schema(description = "创建时间") + private Date createTime; + + /** + * 更新人 + */ + @JsonSerialize(using = ToStringSerializer.class) + @Schema(description = "更新人") + private Long updateUser; + + /** + * 更新时间 + */ + @DateTimeFormat(pattern = DateUtil.PATTERN_DATETIME) + @JsonFormat(pattern = DateUtil.PATTERN_DATETIME) + @Schema(description = "更新时间") + private Date updateTime; + + /** + * 状态[1:正常] + */ + @Schema(description = "业务状态") + private Integer status; + + /** + * 状态[0:未删除,1:删除] + */ + @TableLogic + @Schema(description = "是否已删除") + private Integer isDeleted; +} diff --git a/blade-starter-mybatis/src/main/java/org/springblade/core/mp/base/BizService.java b/blade-starter-mybatis/src/main/java/org/springblade/core/mp/base/BizService.java new file mode 100644 index 0000000..f6166b3 --- /dev/null +++ b/blade-starter-mybatis/src/main/java/org/springblade/core/mp/base/BizService.java @@ -0,0 +1,78 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.mp.base; + +import com.baomidou.mybatisplus.core.toolkit.support.SFunction; +import com.baomidou.mybatisplus.extension.service.IService; + +import jakarta.validation.constraints.NotEmpty; +import java.util.List; + +/** + * 自定义业务接口(推荐自行修改拓展并用于特定业务模块) + * + * @param + * @author Chill + */ +public interface BizService extends IService { + + /** + * 逻辑删除 + * + * @param ids id集合 + * @return + */ + boolean deleteLogic(@NotEmpty List ids); + + /** + * 变更状态 + * + * @param ids id集合 + * @param status 状态值 + * @return + */ + boolean changeStatus(@NotEmpty List ids, Integer status); + + /** + * 判断是否有字段重复 + * + * @param field 字段 + * @param value 值 + * @return boolean + */ + boolean isFieldDuplicate(SFunction field, Object value); + + /** + * 判断是否有字段重复 + * + * @param field 字段 + * @param value 值 + * @param excludedId 排除的id + * @return boolean + */ + boolean isFieldDuplicate(SFunction field, Object value, Long excludedId); + +} diff --git a/blade-starter-mybatis/src/main/java/org/springblade/core/mp/base/BizServiceImpl.java b/blade-starter-mybatis/src/main/java/org/springblade/core/mp/base/BizServiceImpl.java new file mode 100644 index 0000000..f47e1c8 --- /dev/null +++ b/blade-starter-mybatis/src/main/java/org/springblade/core/mp/base/BizServiceImpl.java @@ -0,0 +1,183 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.mp.base; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.core.toolkit.support.SFunction; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.SneakyThrows; +import org.springblade.core.secure.BladeUser; +import org.springblade.core.secure.utils.AuthUtil; +import org.springblade.core.tool.constant.BladeConstant; +import org.springblade.core.tool.utils.*; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import jakarta.validation.constraints.NotEmpty; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.List; + +/** + * 自定义业务封装基础类(推荐自行修改拓展并用于特定业务模块) + * + * @param mapper + * @param model + * @author Chill + */ +@Validated +public class BizServiceImpl, T extends BizEntity> extends ServiceImpl implements BizService { + + @Override + public boolean save(T entity) { + this.resolveEntity(entity); + return super.save(entity); + } + + @Override + public boolean saveBatch(Collection entityList, int batchSize) { + entityList.forEach(this::resolveEntity); + return super.saveBatch(entityList, batchSize); + } + + @Override + public boolean updateById(T entity) { + this.resolveEntity(entity); + return super.updateById(entity); + } + + @Override + public boolean updateBatchById(Collection entityList, int batchSize) { + entityList.forEach(this::resolveEntity); + return super.updateBatchById(entityList, batchSize); + } + + @Override + public boolean saveOrUpdate(T entity) { + if (entity.getId() == null) { + return this.save(entity); + } else { + return this.updateById(entity); + } + } + + @Override + public boolean saveOrUpdateBatch(Collection entityList, int batchSize) { + entityList.forEach(this::resolveEntity); + return super.saveOrUpdateBatch(entityList, batchSize); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean deleteLogic(@NotEmpty List ids) { + BladeUser user = AuthUtil.getUser(); + List list = new ArrayList<>(); + ids.forEach(id -> { + T entity = BeanUtil.newInstance(currentModelClass()); + if (user != null) { + entity.setUpdateUser(user.getUserId()); + } + entity.setUpdateTime(DateUtil.now()); + entity.setId(id); + list.add(entity); + }); + return super.updateBatchById(list) && super.removeByIds(ids); + } + + @Override + public boolean changeStatus(@NotEmpty List ids, Integer status) { + BladeUser user = AuthUtil.getUser(); + List list = new ArrayList<>(); + ids.forEach(id -> { + T entity = BeanUtil.newInstance(currentModelClass()); + if (user != null) { + entity.setUpdateUser(user.getUserId()); + } + entity.setUpdateTime(DateUtil.now()); + entity.setId(id); + entity.setStatus(status); + list.add(entity); + }); + return super.updateBatchById(list); + } + + @Override + public boolean isFieldDuplicate(SFunction field, Object value) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(field, value); + return super.count(queryWrapper) > 0; + } + + @Override + public boolean isFieldDuplicate(SFunction field, Object value, Long excludedId) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(field, value); + if (excludedId != null) { + queryWrapper.ne(T::getId, excludedId); + } + return super.count(queryWrapper) > 0; + } + + @SneakyThrows + private void resolveEntity(T entity) { + BladeUser user = AuthUtil.getUser(); + Date now = DateUtil.now(); + if (entity.getId() == null) { + // 处理新增逻辑 + if (user != null) { + entity.setCreateUser(user.getUserId()); + entity.setCreateDept(Func.firstLong(user.getDeptId())); + entity.setUpdateUser(user.getUserId()); + } + if (entity.getStatus() == null) { + entity.setStatus(BladeConstant.DB_STATUS_NORMAL); + } + entity.setCreateTime(now); + } else if (user != null) { + // 处理修改逻辑 + entity.setUpdateUser(user.getUserId()); + } + // 处理通用逻辑 + entity.setUpdateTime(now); + entity.setIsDeleted(BladeConstant.DB_NOT_DELETED); + // 处理多租户逻辑,若字段值为空,则不进行操作 + Field field = ReflectUtil.getField(entity.getClass(), BladeConstant.DB_TENANT_KEY); + if (ObjectUtil.isNotEmpty(field)) { + Method getTenantId = ClassUtil.getMethod(entity.getClass(), BladeConstant.DB_TENANT_KEY_GET_METHOD); + String tenantId = String.valueOf(getTenantId.invoke(entity)); + if (ObjectUtil.isEmpty(tenantId)) { + Method setTenantId = ClassUtil.getMethod(entity.getClass(), BladeConstant.DB_TENANT_KEY_SET_METHOD, String.class); + setTenantId.invoke(entity, (Object) null); + } + } + } + +} diff --git a/blade-starter-mybatis/src/main/java/org/springblade/core/mp/config/MybatisPlusConfiguration.java b/blade-starter-mybatis/src/main/java/org/springblade/core/mp/config/MybatisPlusConfiguration.java new file mode 100644 index 0000000..50e654a --- /dev/null +++ b/blade-starter-mybatis/src/main/java/org/springblade/core/mp/config/MybatisPlusConfiguration.java @@ -0,0 +1,143 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.mp.config; + +import com.baomidou.mybatisplus.core.injector.ISqlInjector; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler; +import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor; +import lombok.AllArgsConstructor; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.StringValue; +import org.mybatis.spring.annotation.MapperScan; +import org.springblade.core.launch.props.BladePropertySource; +import org.springblade.core.mp.injector.BladeSqlInjector; +import org.springblade.core.mp.intercept.QueryInterceptor; +import org.springblade.core.mp.plugins.BladePaginationInterceptor; +import org.springblade.core.mp.plugins.SqlLogInterceptor; +import org.springblade.core.mp.props.MybatisPlusProperties; +import org.springblade.core.mp.resolver.PageArgumentResolver; +import org.springblade.core.secure.utils.AuthUtil; +import org.springblade.core.tool.constant.BladeConstant; +import org.springblade.core.tool.utils.Func; +import org.springblade.core.tool.utils.ObjectUtil; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.List; + +/** + * mybatis-plus 配置 + * + * @author Chill + */ +@AutoConfiguration +@AllArgsConstructor +@MapperScan("org.springblade.**.mapper.**") +@EnableConfigurationProperties(MybatisPlusProperties.class) +@BladePropertySource(value = "classpath:/blade-mybatis.yml") +public class MybatisPlusConfiguration implements WebMvcConfigurer { + + /** + * 租户拦截器 + */ + @Bean + @ConditionalOnMissingBean(TenantLineInnerInterceptor.class) + public TenantLineInnerInterceptor tenantLineInnerInterceptor() { + return new TenantLineInnerInterceptor(new TenantLineHandler() { + @Override + public Expression getTenantId() { + return new StringValue(Func.toStr(AuthUtil.getTenantId(), BladeConstant.ADMIN_TENANT_ID)); + } + + @Override + public boolean ignoreTable(String tableName) { + return true; + } + }); + } + + /** + * mybatis-plus 拦截器集合 + */ + @Bean + @ConditionalOnMissingBean(MybatisPlusInterceptor.class) + public MybatisPlusInterceptor mybatisPlusInterceptor(ObjectProvider queryInterceptors, + TenantLineInnerInterceptor tenantLineInnerInterceptor, + MybatisPlusProperties mybatisPlusProperties) { + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + // 配置租户拦截器 + if (mybatisPlusProperties.getTenantMode()) { + interceptor.addInnerInterceptor(tenantLineInnerInterceptor); + } + // 配置分页拦截器 + BladePaginationInterceptor paginationInterceptor = new BladePaginationInterceptor(); + // 配置自定义查询拦截器 + QueryInterceptor[] queryInterceptorArray = queryInterceptors.getIfAvailable(); + if (ObjectUtil.isNotEmpty(queryInterceptorArray)) { + AnnotationAwareOrderComparator.sort(queryInterceptorArray); + paginationInterceptor.setQueryInterceptors(queryInterceptorArray); + } + paginationInterceptor.setMaxLimit(mybatisPlusProperties.getPageLimit()); + paginationInterceptor.setOverflow(mybatisPlusProperties.getOverflow()); + paginationInterceptor.setOptimizeJoin(mybatisPlusProperties.getOptimizeJoin()); + interceptor.addInnerInterceptor(paginationInterceptor); + return interceptor; + } + + /** + * sql 日志 + */ + @Bean + public SqlLogInterceptor sqlLogInterceptor(MybatisPlusProperties mybatisPlusProperties) { + return new SqlLogInterceptor(mybatisPlusProperties); + } + + /** + * sql 注入 + */ + @Bean + @ConditionalOnMissingBean(ISqlInjector.class) + public ISqlInjector sqlInjector() { + return new BladeSqlInjector(); + } + + /** + * page 解析器 + */ + @Override + public void addArgumentResolvers(List argumentResolvers) { + argumentResolvers.add(new PageArgumentResolver()); + } + +} + diff --git a/blade-starter-mybatis/src/main/java/org/springblade/core/mp/injector/BladeSqlInjector.java b/blade-starter-mybatis/src/main/java/org/springblade/core/mp/injector/BladeSqlInjector.java new file mode 100644 index 0000000..3fe9e59 --- /dev/null +++ b/blade-starter-mybatis/src/main/java/org/springblade/core/mp/injector/BladeSqlInjector.java @@ -0,0 +1,57 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.mp.injector; + +import com.baomidou.mybatisplus.annotation.FieldFill; +import com.baomidou.mybatisplus.core.injector.AbstractMethod; +import com.baomidou.mybatisplus.core.injector.DefaultSqlInjector; +import com.baomidou.mybatisplus.core.metadata.TableInfo; +import com.baomidou.mybatisplus.extension.injector.methods.InsertBatchSomeColumn; +import org.springblade.core.mp.injector.methods.InsertIgnore; +import org.springblade.core.mp.injector.methods.Replace; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * 自定义的 sql 注入 + * + * @author L.cm + */ +public class BladeSqlInjector extends DefaultSqlInjector { + + @Override + public List getMethodList(Class mapperClass, TableInfo tableInfo) { + List methodList = new ArrayList<>(); + methodList.add(new InsertIgnore()); + methodList.add(new Replace()); + methodList.add(new InsertBatchSomeColumn(i -> i.getFieldFill() != FieldFill.UPDATE)); + methodList.addAll(super.getMethodList(mapperClass, tableInfo)); + return Collections.unmodifiableList(methodList); + } +} diff --git a/blade-starter-mybatis/src/main/java/org/springblade/core/mp/injector/BladeSqlMethod.java b/blade-starter-mybatis/src/main/java/org/springblade/core/mp/injector/BladeSqlMethod.java new file mode 100644 index 0000000..c8a0ce7 --- /dev/null +++ b/blade-starter-mybatis/src/main/java/org/springblade/core/mp/injector/BladeSqlMethod.java @@ -0,0 +1,56 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.mp.injector; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 扩展的自定义方法 + * + * AbstractInsertMethod + * + * @author L.cm + */ +@Getter +@AllArgsConstructor +public enum BladeSqlMethod { + + /** + * 插入如果中已经存在相同的记录,则忽略当前新数据 + */ + INSERT_IGNORE_ONE("insertIgnore", "插入一条数据(选择字段插入)", ""), + + /** + * 表示插入替换数据,需求表中有PrimaryKey,或者unique索引,如果数据库已经存在数据,则用新数据替换,如果没有数据效果则和insert into一样; + */ + REPLACE_ONE("replace", "插入一条数据(选择字段插入)", ""); + + private final String method; + private final String desc; + private final String sql; +} diff --git a/blade-starter-mybatis/src/main/java/org/springblade/core/mp/injector/methods/AbstractInsertMethod.java b/blade-starter-mybatis/src/main/java/org/springblade/core/mp/injector/methods/AbstractInsertMethod.java new file mode 100644 index 0000000..ab7213d --- /dev/null +++ b/blade-starter-mybatis/src/main/java/org/springblade/core/mp/injector/methods/AbstractInsertMethod.java @@ -0,0 +1,83 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.mp.injector.methods; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.core.injector.AbstractMethod; +import com.baomidou.mybatisplus.core.metadata.TableInfo; +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.core.toolkit.sql.SqlScriptUtils; +import org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator; +import org.apache.ibatis.executor.keygen.KeyGenerator; +import org.apache.ibatis.executor.keygen.NoKeyGenerator; +import org.apache.ibatis.mapping.MappedStatement; +import org.apache.ibatis.mapping.SqlSource; +import org.springblade.core.mp.injector.BladeSqlMethod; + +/** + * 抽象的 插入一条数据(选择字段插入) + * + * @author L.cm + */ +public class AbstractInsertMethod extends AbstractMethod { + private final BladeSqlMethod sqlMethod; + + public AbstractInsertMethod(BladeSqlMethod sqlMethod) { + super(sqlMethod.getMethod()); + this.sqlMethod = sqlMethod; + } + + @Override + public MappedStatement injectMappedStatement(Class mapperClass, Class modelClass, TableInfo tableInfo) { + KeyGenerator keyGenerator = new NoKeyGenerator(); + String columnScript = SqlScriptUtils.convertTrim(tableInfo.getAllInsertSqlColumnMaybeIf(null), + LEFT_BRACKET, RIGHT_BRACKET, null, COMMA); + String valuesScript = SqlScriptUtils.convertTrim(tableInfo.getAllInsertSqlPropertyMaybeIf(null), + LEFT_BRACKET, RIGHT_BRACKET, null, COMMA); + String keyProperty = null; + String keyColumn = null; + // 表包含主键处理逻辑,如果不包含主键当普通字段处理 + if (StringUtils.isNotBlank(tableInfo.getKeyProperty())) { + if (tableInfo.getIdType() == IdType.AUTO) { + // 自增主键 + keyGenerator = new Jdbc3KeyGenerator(); + keyProperty = tableInfo.getKeyProperty(); + keyColumn = tableInfo.getKeyColumn(); + } else { + if (null != tableInfo.getKeySequence()) { + keyGenerator = TableInfoHelper.genKeyGenerator(sqlMethod.getMethod(), tableInfo, builderAssistant); + keyProperty = tableInfo.getKeyProperty(); + keyColumn = tableInfo.getKeyColumn(); + } + } + } + String sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), columnScript, valuesScript); + SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass); + return this.addInsertMappedStatement(mapperClass, modelClass, sqlMethod.getMethod(), sqlSource, keyGenerator, keyProperty, keyColumn); + } +} diff --git a/blade-starter-mybatis/src/main/java/org/springblade/core/mp/injector/methods/InsertIgnore.java b/blade-starter-mybatis/src/main/java/org/springblade/core/mp/injector/methods/InsertIgnore.java new file mode 100644 index 0000000..30818fe --- /dev/null +++ b/blade-starter-mybatis/src/main/java/org/springblade/core/mp/injector/methods/InsertIgnore.java @@ -0,0 +1,42 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.mp.injector.methods; + + +import org.springblade.core.mp.injector.BladeSqlMethod; + +/** + * 插入一条数据(选择字段插入)插入如果中已经存在相同的记录,则忽略当前新数据 + * + * @author L.cm + */ +public class InsertIgnore extends AbstractInsertMethod { + + public InsertIgnore() { + super(BladeSqlMethod.INSERT_IGNORE_ONE); + } +} diff --git a/blade-starter-mybatis/src/main/java/org/springblade/core/mp/injector/methods/Replace.java b/blade-starter-mybatis/src/main/java/org/springblade/core/mp/injector/methods/Replace.java new file mode 100644 index 0000000..f7c7b7e --- /dev/null +++ b/blade-starter-mybatis/src/main/java/org/springblade/core/mp/injector/methods/Replace.java @@ -0,0 +1,45 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.mp.injector.methods; + + +import org.springblade.core.mp.injector.BladeSqlMethod; + +/** + * 插入一条数据(选择字段插入) + *

+ * 表示插入替换数据,需求表中有PrimaryKey,或者unique索引,如果数据库已经存在数据,则用新数据替换,如果没有数据效果则和insert into一样; + *

+ * + * @author L.cm + */ +public class Replace extends AbstractInsertMethod { + + public Replace() { + super(BladeSqlMethod.REPLACE_ONE); + } +} + diff --git a/blade-starter-mybatis/src/main/java/org/springblade/core/mp/intercept/QueryInterceptor.java b/blade-starter-mybatis/src/main/java/org/springblade/core/mp/intercept/QueryInterceptor.java new file mode 100644 index 0000000..b8c41a0 --- /dev/null +++ b/blade-starter-mybatis/src/main/java/org/springblade/core/mp/intercept/QueryInterceptor.java @@ -0,0 +1,65 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.mp.intercept; + +import org.apache.ibatis.executor.Executor; +import org.apache.ibatis.mapping.BoundSql; +import org.apache.ibatis.mapping.MappedStatement; +import org.apache.ibatis.session.ResultHandler; +import org.apache.ibatis.session.RowBounds; +import org.springframework.core.Ordered; + +/** + * 自定义 mybatis plus 查询拦截器 + * + * @author L.cm + */ +@SuppressWarnings({"rawtypes"}) +public interface QueryInterceptor extends Ordered { + + /** + * 拦截处理 + * + * @param executor + * @param ms + * @param parameter + * @param rowBounds + * @param resultHandler + * @param boundSql + */ + void intercept(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql); + + /** + * 排序 + * + * @return int + */ + @Override + default int getOrder() { + return Ordered.LOWEST_PRECEDENCE; + } +} diff --git a/blade-starter-mybatis/src/main/java/org/springblade/core/mp/mapper/BladeMapper.java b/blade-starter-mybatis/src/main/java/org/springblade/core/mp/mapper/BladeMapper.java new file mode 100644 index 0000000..c256143 --- /dev/null +++ b/blade-starter-mybatis/src/main/java/org/springblade/core/mp/mapper/BladeMapper.java @@ -0,0 +1,62 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.mp.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +import java.util.List; + +/** + * 自定义的 Mapper + * + * @author L.cm + */ +public interface BladeMapper extends BaseMapper { + + /** + * 插入如果中已经存在相同的记录,则忽略当前新数据 + * + * @param entity 实体对象 + * @return 更改的条数 + */ + int insertIgnore(T entity); + + /** + * 表示插入替换数据,需求表中有PrimaryKey,或者unique索引,如果数据库已经存在数据,则用新数据替换,如果没有数据效果则和insert into一样; + * + * @param entity 实体对象 + * @return 更改的条数 + */ + int replace(T entity); + + /** + * 插入(批量) + * + * @param entityList 实体对象集合 + * @return 成功行数 + */ + int insertBatchSomeColumn(List entityList); +} diff --git a/blade-starter-mybatis/src/main/java/org/springblade/core/mp/plugins/BladePaginationInterceptor.java b/blade-starter-mybatis/src/main/java/org/springblade/core/mp/plugins/BladePaginationInterceptor.java new file mode 100644 index 0000000..84cbede --- /dev/null +++ b/blade-starter-mybatis/src/main/java/org/springblade/core/mp/plugins/BladePaginationInterceptor.java @@ -0,0 +1,86 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.mp.plugins; + +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import com.baomidou.mybatisplus.extension.plugins.pagination.dialects.IDialect; +import com.baomidou.mybatisplus.extension.plugins.pagination.dialects.OracleDialect; +import lombok.Setter; +import lombok.SneakyThrows; +import org.apache.ibatis.executor.Executor; +import org.apache.ibatis.mapping.BoundSql; +import org.apache.ibatis.mapping.MappedStatement; +import org.apache.ibatis.session.ResultHandler; +import org.apache.ibatis.session.RowBounds; +import org.springblade.core.mp.intercept.QueryInterceptor; + +import java.sql.Connection; + +/** + * 拓展分页拦截器 + * + * @author Chill + */ +@Setter +public class BladePaginationInterceptor extends PaginationInnerInterceptor { + + /** + * 查询拦截器 + */ + private QueryInterceptor[] queryInterceptors; + + @SneakyThrows + @Override + public boolean willDoQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { + QueryInterceptorExecutor.exec(queryInterceptors, executor, ms, parameter, rowBounds, resultHandler, boundSql); + return super.willDoQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql); + } + + @SneakyThrows + @Override + public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { + super.setDialect(autoDialect(executor)); + super.beforeQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql); + } + + /** + * 自动配置分页方言类的逻辑 + * + * @param executor Executor + * @return 分页方言类 + */ + @SneakyThrows + protected IDialect autoDialect(Executor executor) { + // 增加YashanDB方言 + Connection conn = executor.getTransaction().getConnection(); + if (conn.getMetaData().getURL().contains(":yasdb:")) { + return new OracleDialect(); + } else { + return super.findIDialect(executor); + } + } + +} diff --git a/blade-starter-mybatis/src/main/java/org/springblade/core/mp/plugins/QueryInterceptorExecutor.java b/blade-starter-mybatis/src/main/java/org/springblade/core/mp/plugins/QueryInterceptorExecutor.java new file mode 100644 index 0000000..9d6aa24 --- /dev/null +++ b/blade-starter-mybatis/src/main/java/org/springblade/core/mp/plugins/QueryInterceptorExecutor.java @@ -0,0 +1,61 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.mp.plugins; + +import org.apache.ibatis.executor.Executor; +import org.apache.ibatis.mapping.BoundSql; +import org.apache.ibatis.mapping.MappedStatement; +import org.apache.ibatis.session.ResultHandler; +import org.apache.ibatis.session.RowBounds; +import org.springblade.core.mp.intercept.QueryInterceptor; +import org.springblade.core.tool.utils.ObjectUtil; + +/** + * 查询拦截器执行器 + * + *

+ * 目的:抽取此方法是为了后期方便同步更新 {@link BladePaginationInterceptor} + *

+ * + * @author L.cm + */ +@SuppressWarnings({"rawtypes"}) +public class QueryInterceptorExecutor { + + /** + * 执行查询拦截器 + */ + static void exec(QueryInterceptor[] interceptors, Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws Throwable { + if (ObjectUtil.isEmpty(interceptors)) { + return; + } + for (QueryInterceptor interceptor : interceptors) { + interceptor.intercept(executor, ms, parameter, rowBounds, resultHandler, boundSql); + } + } + +} diff --git a/blade-starter-mybatis/src/main/java/org/springblade/core/mp/plugins/SqlLogInterceptor.java b/blade-starter-mybatis/src/main/java/org/springblade/core/mp/plugins/SqlLogInterceptor.java new file mode 100644 index 0000000..bf368a8 --- /dev/null +++ b/blade-starter-mybatis/src/main/java/org/springblade/core/mp/plugins/SqlLogInterceptor.java @@ -0,0 +1,173 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.mp.plugins; + +import com.alibaba.druid.DbType; +import com.alibaba.druid.filter.FilterChain; +import com.alibaba.druid.filter.FilterEventAdapter; +import com.alibaba.druid.proxy.jdbc.JdbcParameter; +import com.alibaba.druid.proxy.jdbc.ResultSetProxy; +import com.alibaba.druid.proxy.jdbc.StatementProxy; +import com.alibaba.druid.sql.SQLUtils; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.mp.props.MybatisPlusProperties; +import org.springblade.core.tool.utils.StringUtil; + +import java.time.temporal.TemporalAccessor; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * 打印可执行的 sql 日志 + * + * @author L.cm,Chill + */ +@Slf4j +public class SqlLogInterceptor extends FilterEventAdapter { + private static final SQLUtils.FormatOption FORMAT_OPTION = new SQLUtils.FormatOption(false, false); + + private static final List SQL_LOG_EXCLUDE = new ArrayList<>(Arrays.asList("ACT_RU_JOB", "ACT_RU_TIMER_JOB")); + + private final MybatisPlusProperties properties; + + public SqlLogInterceptor(MybatisPlusProperties properties) { + this.properties = properties; + if (properties.getSqlLogExclude().size() > 0) { + SQL_LOG_EXCLUDE.addAll(properties.getSqlLogExclude()); + } + } + + @Override + protected void statementExecuteBefore(StatementProxy statement, String sql) { + statement.setLastExecuteStartNano(); + } + + @Override + protected void statementExecuteBatchBefore(StatementProxy statement) { + statement.setLastExecuteStartNano(); + } + + @Override + protected void statementExecuteUpdateBefore(StatementProxy statement, String sql) { + statement.setLastExecuteStartNano(); + } + + @Override + protected void statementExecuteQueryBefore(StatementProxy statement, String sql) { + statement.setLastExecuteStartNano(); + } + + @Override + protected void statementExecuteAfter(StatementProxy statement, String sql, boolean firstResult) { + statement.setLastExecuteTimeNano(); + } + + @Override + protected void statementExecuteBatchAfter(StatementProxy statement, int[] result) { + statement.setLastExecuteTimeNano(); + } + + @Override + protected void statementExecuteQueryAfter(StatementProxy statement, String sql, ResultSetProxy resultSet) { + statement.setLastExecuteTimeNano(); + } + + @Override + protected void statementExecuteUpdateAfter(StatementProxy statement, String sql, int updateCount) { + statement.setLastExecuteTimeNano(); + } + + @Override + @SneakyThrows + public void statement_close(FilterChain chain, StatementProxy statement) { + // 是否开启日志 + if (!properties.getSqlLog()) { + chain.statement_close(statement); + return; + } + // 是否开启调试 + if (!log.isInfoEnabled()) { + chain.statement_close(statement); + return; + } + // 打印可执行的 sql + String sql = statement.getBatchSql(); + // sql 为空直接返回 + if (StringUtil.isEmpty(sql)) { + chain.statement_close(statement); + return; + } + // sql 包含排除的关键字直接返回 + if (excludeSql(sql)) { + chain.statement_close(statement); + return; + } + int parametersSize = statement.getParametersSize(); + List parameters = new ArrayList<>(parametersSize); + for (int i = 0; i < parametersSize; ++i) { + // 转换参数,处理 java8 时间 + parameters.add(getJdbcParameter(statement.getParameter(i))); + } + String dbType = statement.getConnectionProxy().getDirectDataSource().getDbType(); + String formattedSql = SQLUtils.format(sql, DbType.of(dbType), parameters, FORMAT_OPTION); + printSql(formattedSql, statement); + chain.statement_close(statement); + } + + private static Object getJdbcParameter(JdbcParameter jdbcParam) { + if (jdbcParam == null) { + return null; + } + Object value = jdbcParam.getValue(); + // 处理 java8 时间 + if (value instanceof TemporalAccessor) { + return value.toString(); + } + return value; + } + + private static void printSql(String sql, StatementProxy statement) { + // 打印 sql + String sqlLogger = "\n\n============== Sql Start ==============" + + "\nExecute SQL : {}" + + "\nExecute Time: {}" + + "\n============== Sql End ==============\n"; + log.info(sqlLogger, sql.trim(), StringUtil.format(statement.getLastExecuteTimeNano())); + } + + private static boolean excludeSql(String sql) { + // 判断关键字 + for (String exclude : SQL_LOG_EXCLUDE) { + if (sql.contains(exclude)) { + return true; + } + } + return false; + } + +} diff --git a/blade-starter-mybatis/src/main/java/org/springblade/core/mp/props/MybatisPlusProperties.java b/blade-starter-mybatis/src/main/java/org/springblade/core/mp/props/MybatisPlusProperties.java new file mode 100644 index 0000000..67925a1 --- /dev/null +++ b/blade-starter-mybatis/src/main/java/org/springblade/core/mp/props/MybatisPlusProperties.java @@ -0,0 +1,73 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.mp.props; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.ArrayList; +import java.util.List; + +/** + * MybatisPlus配置类 + * + * @author Chill + */ +@Data +@ConfigurationProperties(prefix = "blade.mybatis-plus") +public class MybatisPlusProperties { + + /** + * 开启租户模式 + */ + private Boolean tenantMode = true; + + /** + * 开启sql日志 + */ + private Boolean sqlLog = true; + + /** + * sql日志忽略打印关键字 + */ + private List sqlLogExclude = new ArrayList<>(); + + /** + * 分页最大数 + */ + private Long pageLimit = 500L; + + /** + * 溢出总页数后是否进行处理 + */ + protected Boolean overflow = false; + + /** + * join优化 + */ + private Boolean optimizeJoin = false; + +} diff --git a/blade-starter-mybatis/src/main/java/org/springblade/core/mp/resolver/PageArgumentResolver.java b/blade-starter-mybatis/src/main/java/org/springblade/core/mp/resolver/PageArgumentResolver.java new file mode 100644 index 0000000..0f173c6 --- /dev/null +++ b/blade-starter-mybatis/src/main/java/org/springblade/core/mp/resolver/PageArgumentResolver.java @@ -0,0 +1,75 @@ +package org.springblade.core.mp.resolver; + +import com.baomidou.mybatisplus.core.metadata.OrderItem; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.springblade.core.tool.utils.ObjectUtil; +import org.springblade.core.tool.utils.StringPool; +import org.springblade.core.tool.utils.StringUtil; +import org.springframework.core.MethodParameter; +import org.springframework.lang.NonNull; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +/** + * 解决 Mybatis Plus page SQL注入问题 + * + * @author L.cm + */ +public class PageArgumentResolver implements HandlerMethodArgumentResolver { + private static final String ORDER_ASC = "asc"; + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return Page.class.equals(parameter.getParameterType()); + } + + /** + * page 参数解析 + * + * @param parameter MethodParameter + * @param mavContainer ModelAndViewContainer + * @param request NativeWebRequest + * @param binderFactory WebDataBinderFactory + * @return 检查后新的page对象 + */ + @Override + public Object resolveArgument(@NonNull MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest request, WebDataBinderFactory binderFactory) { + // 分页参数 page: 0, size: 10, sort=id%2Cdesc + String pageParam = request.getParameter("page"); + String sizeParam = request.getParameter("size"); + String[] sortParam = request.getParameterValues("sort"); + Page page = new Page<>(); + if (StringUtil.isNotBlank(pageParam)) { + page.setCurrent(Long.parseLong(pageParam)); + } + if (StringUtil.isNotBlank(sizeParam)) { + page.setSize(Long.parseLong(sizeParam)); + } + if (ObjectUtil.isEmpty(sortParam)) { + return page; + } + for (String param : sortParam) { + if (StringUtil.isBlank(param)) { + continue; + } + String[] split = param.split(StringPool.COMMA); + // 清理字符串 + OrderItem orderItem = new OrderItem(); + orderItem.setColumn(StringUtil.cleanIdentifier(split[0])); + orderItem.setAsc(isOrderAsc(split)); + page.addOrder(orderItem); + } + return page; + } + + private static boolean isOrderAsc(String[] split) { + // 默认 desc + if (split.length < 2) { + return false; + } + return ORDER_ASC.equalsIgnoreCase(split[1]); + } +} diff --git a/blade-starter-mybatis/src/main/java/org/springblade/core/mp/service/BladeService.java b/blade-starter-mybatis/src/main/java/org/springblade/core/mp/service/BladeService.java new file mode 100644 index 0000000..fd3242a --- /dev/null +++ b/blade-starter-mybatis/src/main/java/org/springblade/core/mp/service/BladeService.java @@ -0,0 +1,97 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.mp.service; + +import org.springblade.core.mp.base.BaseService; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Collection; +import java.util.List; + +/** + * 自定义的 Service + * + * @author L.cm, chill + */ +public interface BladeService extends BaseService { + + /** + * 插入如果中已经存在相同的记录,则忽略当前新数据 + * + * @param entity entity + * @return 是否成功 + */ + boolean saveIgnore(T entity); + + /** + * 表示插入替换数据,需求表中有PrimaryKey,或者unique索引,如果数据库已经存在数据,则用新数据替换,如果没有数据效果则和insert into一样; + * + * @param entity entity + * @return 是否成功 + */ + boolean saveReplace(T entity); + + /** + * 插入(批量),插入如果中已经存在相同的记录,则忽略当前新数据 + * + * @param entityList 实体对象集合 + * @param batchSize 批次大小 + * @return 是否成功 + */ + boolean saveIgnoreBatch(Collection entityList, int batchSize); + + /** + * 插入(批量),插入如果中已经存在相同的记录,则忽略当前新数据 + * + * @param entityList 实体对象集合 + * @return 是否成功 + */ + @Transactional(rollbackFor = Exception.class) + default boolean saveIgnoreBatch(Collection entityList) { + return saveIgnoreBatch(entityList, 1000); + } + + /** + * 插入(批量),表示插入替换数据,需求表中有PrimaryKey,或者unique索引,如果数据库已经存在数据,则用新数据替换,如果没有数据效果则和insert into一样; + * + * @param entityList 实体对象集合 + * @param batchSize 批次大小 + * @return 是否成功 + */ + boolean saveReplaceBatch(Collection entityList, int batchSize); + + /** + * 插入(批量),表示插入替换数据,需求表中有PrimaryKey,或者unique索引,如果数据库已经存在数据,则用新数据替换,如果没有数据效果则和insert into一样; + * + * @param entityList 实体对象集合 + * @return 是否成功 + */ + @Transactional(rollbackFor = Exception.class) + default boolean saveReplaceBatch(Collection entityList) { + return saveReplaceBatch(entityList, 1000); + } +} diff --git a/blade-starter-mybatis/src/main/java/org/springblade/core/mp/service/impl/BladeServiceImpl.java b/blade-starter-mybatis/src/main/java/org/springblade/core/mp/service/impl/BladeServiceImpl.java new file mode 100644 index 0000000..cdd3309 --- /dev/null +++ b/blade-starter-mybatis/src/main/java/org/springblade/core/mp/service/impl/BladeServiceImpl.java @@ -0,0 +1,85 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.mp.service.impl; + +import com.baomidou.mybatisplus.extension.toolkit.SqlHelper; +import org.springblade.core.mp.base.BaseEntity; +import org.springblade.core.mp.base.BaseServiceImpl; +import org.springblade.core.mp.injector.BladeSqlMethod; +import org.springblade.core.mp.mapper.BladeMapper; +import org.springblade.core.mp.service.BladeService; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.util.Collection; + +/** + * BladeService 实现类( 泛型:M 是 mapper 对象,T 是实体 , PK 是主键泛型 ) + * + * @author L.cm, chill + */ +@Validated +public class BladeServiceImpl, T extends BaseEntity> extends BaseServiceImpl implements BladeService { + + @Override + public boolean saveIgnore(T entity) { + return SqlHelper.retBool(baseMapper.insertIgnore(entity)); + } + + @Override + public boolean saveReplace(T entity) { + return SqlHelper.retBool(baseMapper.replace(entity)); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean saveIgnoreBatch(Collection entityList, int batchSize) { + return saveBatch(entityList, batchSize, BladeSqlMethod.INSERT_IGNORE_ONE); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean saveReplaceBatch(Collection entityList, int batchSize) { + return saveBatch(entityList, batchSize, BladeSqlMethod.REPLACE_ONE); + } + + private boolean saveBatch(Collection entityList, int batchSize, BladeSqlMethod sqlMethod) { + String sqlStatement = bladeSqlStatement(sqlMethod); + executeBatch(entityList, batchSize, (sqlSession, entity) -> sqlSession.insert(sqlStatement, entity)); + return true; + } + + /** + * 获取 bladeSqlStatement + * + * @param sqlMethod ignore + * @return sql + */ + protected String bladeSqlStatement(BladeSqlMethod sqlMethod) { + return SqlHelper.table(currentModelClass()).getSqlStatement(sqlMethod.getMethod()); + } +} diff --git a/blade-starter-mybatis/src/main/java/org/springblade/core/mp/support/BaseEntityWrapper.java b/blade-starter-mybatis/src/main/java/org/springblade/core/mp/support/BaseEntityWrapper.java new file mode 100644 index 0000000..8b9f29e --- /dev/null +++ b/blade-starter-mybatis/src/main/java/org/springblade/core/mp/support/BaseEntityWrapper.java @@ -0,0 +1,72 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.mp.support; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * 视图包装基类 + * + * @author Chill + */ +public abstract class BaseEntityWrapper { + + /** + * 单个实体类包装 + * + * @param entity 实体类 + * @return V + */ + public abstract V entityVO(E entity); + + /** + * 实体类集合包装 + * + * @param list 实体类集合 + * @return List + */ + public List listVO(List list) { + return list.stream().map(this::entityVO).collect(Collectors.toList()); + } + + /** + * 分页实体类集合包装 + * + * @param pages 分页对象 + * @return IPage + */ + public IPage pageVO(IPage pages) { + List records = listVO(pages.getRecords()); + IPage pageVo = new Page<>(pages.getCurrent(), pages.getSize(), pages.getTotal()); + pageVo.setRecords(records); + return pageVo; + } + +} diff --git a/blade-starter-mybatis/src/main/java/org/springblade/core/mp/support/BladePage.java b/blade-starter-mybatis/src/main/java/org/springblade/core/mp/support/BladePage.java new file mode 100644 index 0000000..3789462 --- /dev/null +++ b/blade-starter-mybatis/src/main/java/org/springblade/core/mp/support/BladePage.java @@ -0,0 +1,81 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.mp.support; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Collections; +import java.util.List; + +/** + * 分页模型 + * + * @author Chill + */ +@Data +public class BladePage implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 查询数据列表 + */ + private List records = Collections.emptyList(); + + /** + * 总数 + */ + private long total = 0; + /** + * 每页显示条数,默认 10 + */ + private long size = 10; + + /** + * 当前页 + */ + private long current = 1; + + /** + * mybatis-plus分页模型转化 + * + * @param page 分页实体类 + * @return BladePage + */ + public static BladePage of(IPage page) { + BladePage bladePage = new BladePage<>(); + bladePage.setRecords(page.getRecords()); + bladePage.setTotal(page.getTotal()); + bladePage.setSize(page.getSize()); + bladePage.setCurrent(page.getCurrent()); + return bladePage; + } + +} diff --git a/blade-starter-mybatis/src/main/java/org/springblade/core/mp/support/Condition.java b/blade-starter-mybatis/src/main/java/org/springblade/core/mp/support/Condition.java new file mode 100644 index 0000000..cb7bbcb --- /dev/null +++ b/blade-starter-mybatis/src/main/java/org/springblade/core/mp/support/Condition.java @@ -0,0 +1,127 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.mp.support; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.metadata.OrderItem; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.springblade.core.launch.constant.TokenConstant; +import org.springblade.core.tool.support.Kv; +import org.springblade.core.tool.utils.BeanUtil; +import org.springblade.core.tool.utils.Func; +import org.springblade.core.tool.utils.StringUtil; + +import java.util.Map; + +/** + * 分页工具 + * + * @author Chill + */ +public class Condition { + + /** + * 转化成mybatis plus中的Page + * + * @param query 查询条件 + * @return IPage + */ + public static IPage getPage(Query query) { + Page page = new Page<>(Func.toInt(query.getCurrent(), 1), Func.toInt(query.getSize(), 10)); + String[] ascArr = Func.toStrArray(query.getAscs()); + for (String asc : ascArr) { + page.addOrder(OrderItem.asc(StringUtil.cleanIdentifier(asc))); + } + String[] descArr = Func.toStrArray(query.getDescs()); + for (String desc : descArr) { + page.addOrder(OrderItem.desc(StringUtil.cleanIdentifier(desc))); + } + return page; + } + + /** + * 转化成mybatis plus中的Page + * + * @param query 查询条件 + * @return IPage + */ + public static IPage getPageDesc(Query query) { + Page page = new Page<>(Func.toInt(query.getCurrent(), 1), Func.toInt(query.getSize(), 10)); + String[] descArr = Func.toStrArray(query.getDescs()); + for (String desc : descArr) { + page.addOrder(OrderItem.desc(StringUtil.cleanIdentifier(desc))); + } + String[] ascArr = Func.toStrArray(query.getAscs()); + for (String asc : ascArr) { + page.addOrder(OrderItem.asc(StringUtil.cleanIdentifier(asc))); + } + return page; + } + + /** + * 获取mybatis plus中的QueryWrapper + * + * @param entity 实体 + * @param 类型 + * @return QueryWrapper + */ + public static QueryWrapper getQueryWrapper(T entity) { + return new QueryWrapper<>(entity); + } + + /** + * 获取mybatis plus中的QueryWrapper + * + * @param query 查询条件 + * @param clazz 实体类 + * @param 类型 + * @return QueryWrapper + */ + public static QueryWrapper getQueryWrapper(Map query, Class clazz) { + Kv exclude = Kv.create().set(TokenConstant.HEADER, TokenConstant.HEADER) + .set("current", "current").set("size", "size").set("ascs", "ascs").set("descs", "descs"); + return getQueryWrapper(query, exclude, clazz); + } + + /** + * 获取mybatis plus中的QueryWrapper + * + * @param query 查询条件 + * @param exclude 排除的查询条件 + * @param clazz 实体类 + * @param 类型 + * @return QueryWrapper + */ + public static QueryWrapper getQueryWrapper(Map query, Map exclude, Class clazz) { + exclude.forEach((k, v) -> query.remove(k)); + QueryWrapper qw = new QueryWrapper<>(); + qw.setEntity(BeanUtil.newInstance(clazz)); + SqlKeyword.buildCondition(query, qw); + return qw; + } + +} diff --git a/blade-starter-mybatis/src/main/java/org/springblade/core/mp/support/Query.java b/blade-starter-mybatis/src/main/java/org/springblade/core/mp/support/Query.java new file mode 100644 index 0000000..e696909 --- /dev/null +++ b/blade-starter-mybatis/src/main/java/org/springblade/core/mp/support/Query.java @@ -0,0 +1,66 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.mp.support; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +/** + * 分页工具 + * + * @author Chill + */ +@Data +@Accessors(chain = true) +@Schema(description = "查询条件") +public class Query { + + /** + * 当前页 + */ + @Schema(description = "当前页") + private Integer current; + + /** + * 每页的数量 + */ + @Schema(description = "每页的数量") + private Integer size; + + /** + * 正排序规则 + */ + @Schema(hidden = true) + private String ascs; + + /** + * 倒排序规则 + */ + @Schema(hidden = true) + private String descs; + +} diff --git a/blade-starter-mybatis/src/main/java/org/springblade/core/mp/support/SqlKeyword.java b/blade-starter-mybatis/src/main/java/org/springblade/core/mp/support/SqlKeyword.java new file mode 100644 index 0000000..64f7b7b --- /dev/null +++ b/blade-starter-mybatis/src/main/java/org/springblade/core/mp/support/SqlKeyword.java @@ -0,0 +1,172 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.mp.support; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import lombok.SneakyThrows; +import org.springblade.core.tool.utils.DateUtil; +import org.springblade.core.tool.utils.Func; +import org.springblade.core.tool.utils.StringPool; +import org.springblade.core.tool.utils.StringUtil; + +import java.sql.SQLException; +import java.util.Map; +import java.util.regex.Pattern; + +/** + * 定义常用的 sql关键字 + * + * @author Chill + */ +public class SqlKeyword { + /** + * 常规sql字符匹配关键词 + */ + private final static String SQL_REGEX = "(?i)(? query, QueryWrapper qw) { + if (Func.isEmpty(query)) { + return; + } + query.forEach((k, v) -> { + if (Func.hasEmpty(k, v) || k.endsWith(IGNORE)) { + return; + } + // 过滤sql注入关键词 + k = filter(k); + if (k.endsWith(EQUAL)) { + qw.eq(getColumn(k, EQUAL), v); + } else if (k.endsWith(NOT_EQUAL)) { + qw.ne(getColumn(k, NOT_EQUAL), v); + } else if (k.endsWith(LIKE_LEFT)) { + qw.likeLeft(getColumn(k, LIKE_LEFT), v); + } else if (k.endsWith(LIKE_RIGHT)) { + qw.likeRight(getColumn(k, LIKE_RIGHT), v); + } else if (k.endsWith(NOT_LIKE)) { + qw.notLike(getColumn(k, NOT_LIKE), v); + } else if (k.endsWith(GE)) { + qw.ge(getColumn(k, GE), v); + } else if (k.endsWith(LE)) { + qw.le(getColumn(k, LE), v); + } else if (k.endsWith(GT)) { + qw.gt(getColumn(k, GT), v); + } else if (k.endsWith(LT)) { + qw.lt(getColumn(k, LT), v); + } else if (k.endsWith(DATE_GE)) { + qw.ge(getColumn(k, DATE_GE), DateUtil.parse(String.valueOf(v), DateUtil.PATTERN_DATETIME)); + } else if (k.endsWith(DATE_GT)) { + qw.gt(getColumn(k, DATE_GT), DateUtil.parse(String.valueOf(v), DateUtil.PATTERN_DATETIME)); + } else if (k.endsWith(DATE_EQUAL)) { + qw.eq(getColumn(k, DATE_EQUAL), DateUtil.parse(String.valueOf(v), DateUtil.PATTERN_DATETIME)); + } else if (k.endsWith(DATE_LE)) { + qw.le(getColumn(k, DATE_LE), DateUtil.parse(String.valueOf(v), DateUtil.PATTERN_DATETIME)); + } else if (k.endsWith(DATE_LT)) { + qw.lt(getColumn(k, DATE_LT), DateUtil.parse(String.valueOf(v), DateUtil.PATTERN_DATETIME)); + } else if (k.endsWith(IS_NULL)) { + qw.isNull(getColumn(k, IS_NULL)); + } else if (k.endsWith(NOT_NULL)) { + qw.isNotNull(getColumn(k, NOT_NULL)); + } else { + qw.like(getColumn(k, LIKE), v); + } + }); + } + + /** + * 获取数据库字段 + * + * @param column 字段名 + * @param keyword 关键字 + * @return + */ + private static String getColumn(String column, String keyword) { + return StringUtil.humpToUnderline(StringUtil.removeSuffix(column, keyword)); + } + + /** + * 把SQL关键字替换为空字符串 + * + * @param param 关键字 + * @return string + */ + @SneakyThrows(SQLException.class) + public static String filter(String param) { + if (param == null) { + return null; + } + // 将校验到的sql关键词替换为空字符串 + String sql = param.replaceAll(SQL_REGEX, StringPool.EMPTY); + // 二次校验,避免双写绕过等情况出现 + if (match(sql)) { + throw new SQLException(SQL_INJECTION_MESSAGE); + } + return sql; + } + + /** + * 判断字符是否包含SQL关键字 + * + * @param param 关键字 + * @return boolean + */ + public static Boolean match(String param) { + return Func.isNotEmpty(param) && PATTERN.matcher(param).find(); + } + +} diff --git a/blade-starter-mybatis/src/main/java/org/springblade/core/mp/utils/PageUtil.java b/blade-starter-mybatis/src/main/java/org/springblade/core/mp/utils/PageUtil.java new file mode 100644 index 0000000..ffb07ed --- /dev/null +++ b/blade-starter-mybatis/src/main/java/org/springblade/core/mp/utils/PageUtil.java @@ -0,0 +1,90 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.mp.utils; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.springblade.core.tool.utils.BeanUtil; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +/** + * 分页工具类 + * + * @author L.cm + */ +public class PageUtil { + + /** + * 2个 IPage 转 Page + * + * @param page IPage + * @param target 需要copy转换的类型 + * @param 泛型 + * @return PageResult + */ + public static Page toPage(IPage page, Class target) { + List records = BeanUtil.copy(page.getRecords(), target); + return toPage(page, records); + } + + /** + * 2个 IPage 转 Page + * + * @param page IPage + * @param records 转换过的list模型 + * @param 泛型 + * @return PageResult + */ + public static Page toPage(IPage page, List records) { + Page pageResult = new Page<>(); + pageResult.setCurrent(page.getCurrent()); + pageResult.setSize(page.getSize()); + pageResult.setPages(page.getPages()); + pageResult.setTotal(page.getTotal()); + pageResult.setRecords(records); + return pageResult; + } + + /** + * Page 转换 + * + * @param page IPage + * @param function 转换过的函数 + * @param 泛型 + * @return PageResult + */ + public static Page toPage(IPage page, Function function) { + List records = new ArrayList<>(); + for (T record : page.getRecords()) { + records.add(function.apply(record)); + } + return toPage(page, records); + } + +} diff --git a/blade-starter-mybatis/src/main/resources/blade-mybatis.yml b/blade-starter-mybatis/src/main/resources/blade-mybatis.yml new file mode 100644 index 0000000..0b39b1c --- /dev/null +++ b/blade-starter-mybatis/src/main/resources/blade-mybatis.yml @@ -0,0 +1,35 @@ +#多数据源Sql日志配置 +spring: + datasource: + dynamic: + druid: + proxy-filters: + - sqlLogInterceptor + +#mybatis-plus配置 +mybatis-plus: + mapper-locations: classpath:org/springblade/**/mapper/*Mapper.xml + #实体扫描,多个package用逗号或者分号分隔 + typeAliasesPackage: org.springblade.**.entity + #typeEnumsPackage: org.springblade.dashboard.entity.enums + global-config: + # 关闭MP3.0自带的banner + banner: false + db-config: + #主键类型 0:"数据库ID自增", 1:"不操作", 2:"用户输入ID",3:"数字型snowflake", 4:"全局唯一ID UUID", 5:"字符串型snowflake"; + id-type: assign_id + #字段策略 + insert-strategy: not_null + update-strategy: not_null + where-strategy: not_null + #驼峰下划线转换 + table-underline: true + # 逻辑删除配置 + # 逻辑删除全局值(1表示已删除,这也是Mybatis Plus的默认配置) + logic-delete-value: 1 + # 逻辑未删除全局值(0表示未删除,这也是Mybatis Plus的默认配置) + logic-not-delete-value: 0 + configuration: + map-underscore-to-camel-case: true + cache-enabled: false + jdbc-type-for-null: 'null' diff --git a/blade-starter-oss/pom.xml b/blade-starter-oss/pom.xml new file mode 100644 index 0000000..52a39af --- /dev/null +++ b/blade-starter-oss/pom.xml @@ -0,0 +1,70 @@ + + + 4.0.0 + + BladeX-Tool + org.springblade + ${revision} + + + blade-starter-oss + ${project.artifactId} + ${project.parent.version} + jar + + + + org.springblade + blade-core-secure + + + + com.aliyun.oss + aliyun-sdk-oss + provided + + + + io.minio + minio + provided + + + + com.qiniu + qiniu-java-sdk + provided + + + + com.qcloud + cos_api + provided + + + + com.huaweicloud + esdk-obs-java + provided + + + + com.amazonaws + aws-java-sdk-s3 + provided + + + com.squareup.okhttp3 + okhttp + provided + + + org.springblade + blade-core-auto + provided + + + + diff --git a/blade-starter-oss/src/main/java/org/springblade/core/oss/AliossTemplate.java b/blade-starter-oss/src/main/java/org/springblade/core/oss/AliossTemplate.java new file mode 100644 index 0000000..5d293ef --- /dev/null +++ b/blade-starter-oss/src/main/java/org/springblade/core/oss/AliossTemplate.java @@ -0,0 +1,357 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oss; + +import com.aliyun.oss.OSSClient; +import com.aliyun.oss.common.utils.BinaryUtil; +import com.aliyun.oss.model.*; +import lombok.AllArgsConstructor; +import lombok.SneakyThrows; +import org.springblade.core.oss.model.BladeFile; +import org.springblade.core.oss.model.OssFile; +import org.springblade.core.oss.props.OssProperties; +import org.springblade.core.oss.rule.OssRule; +import org.springblade.core.tool.jackson.JsonUtil; +import org.springblade.core.tool.utils.StringPool; +import org.springblade.core.tool.utils.StringUtil; +import org.springframework.util.StringUtils; +import org.springframework.web.multipart.MultipartFile; + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * AliossTemplate + * + * @author Chill + */ +@AllArgsConstructor +public class AliossTemplate implements OssTemplate { + private final OSSClient ossClient; + private final OssProperties ossProperties; + private final OssRule ossRule; + + @Override + @SneakyThrows + public void makeBucket(String bucketName) { + if (!bucketExists(bucketName)) { + ossClient.createBucket(getBucketName(bucketName)); + } + } + + @Override + @SneakyThrows + public void removeBucket(String bucketName) { + ossClient.deleteBucket(getBucketName(bucketName)); + } + + @Override + @SneakyThrows + public boolean bucketExists(String bucketName) { + return ossClient.doesBucketExist(getBucketName(bucketName)); + } + + @Override + @SneakyThrows + public void copyFile(String bucketName, String fileName, String destBucketName) { + ossClient.copyObject(getBucketName(bucketName), fileName, getBucketName(destBucketName), fileName); + } + + @Override + @SneakyThrows + public void copyFile(String bucketName, String fileName, String destBucketName, String destFileName) { + ossClient.copyObject(getBucketName(bucketName), fileName, getBucketName(destBucketName), destFileName); + } + + @Override + @SneakyThrows + public OssFile statFile(String fileName) { + return statFile(ossProperties.getBucketName(), fileName); + } + + @Override + @SneakyThrows + public OssFile statFile(String bucketName, String fileName) { + ObjectMetadata stat = ossClient.getObjectMetadata(getBucketName(bucketName), fileName); + OssFile ossFile = new OssFile(); + ossFile.setName(fileName); + ossFile.setLink(fileLink(ossFile.getName())); + ossFile.setHash(stat.getContentMD5()); + ossFile.setLength(stat.getContentLength()); + ossFile.setPutTime(stat.getLastModified()); + ossFile.setContentType(stat.getContentType()); + return ossFile; + } + + @Override + @SneakyThrows + public String filePath(String fileName) { + return getOssHost().concat(StringPool.SLASH).concat(fileName); + } + + @Override + @SneakyThrows + public String filePath(String bucketName, String fileName) { + return getOssHost(bucketName).concat(StringPool.SLASH).concat(fileName); + } + + @Override + @SneakyThrows + public String fileLink(String fileName) { + return getOssHost().concat(StringPool.SLASH).concat(fileName); + } + + @Override + @SneakyThrows + public String fileLink(String bucketName, String fileName) { + return getOssHost(bucketName).concat(StringPool.SLASH).concat(fileName); + } + + /** + * 文件对象 + * + * @param file 上传文件类 + * @return + */ + @Override + @SneakyThrows + public BladeFile putFile(MultipartFile file) { + return putFile(ossProperties.getBucketName(), file.getOriginalFilename(), file); + } + + /** + * @param fileName 上传文件名 + * @param file 上传文件类 + * @return + */ + @Override + @SneakyThrows + public BladeFile putFile(String fileName, MultipartFile file) { + return putFile(ossProperties.getBucketName(), fileName, file); + } + + @Override + @SneakyThrows + public BladeFile putFile(String bucketName, String fileName, MultipartFile file) { + return putFile(bucketName, fileName, file.getInputStream()); + } + + @Override + @SneakyThrows + public BladeFile putFile(String fileName, InputStream stream) { + return putFile(ossProperties.getBucketName(), fileName, stream); + } + + @Override + @SneakyThrows + public BladeFile putFile(String bucketName, String fileName, InputStream stream) { + return put(bucketName, stream, fileName, false); + } + + @SneakyThrows + public BladeFile put(String bucketName, InputStream stream, String key, boolean cover) { + makeBucket(bucketName); + String originalName = key; + key = getFileName(key); + // 覆盖上传 + if (cover) { + ossClient.putObject(getBucketName(bucketName), key, stream); + } else { + PutObjectResult response = ossClient.putObject(getBucketName(bucketName), key, stream); + int retry = 0; + int retryCount = 5; + while (StringUtils.isEmpty(response.getETag()) && retry < retryCount) { + response = ossClient.putObject(getBucketName(bucketName), key, stream); + retry++; + } + } + BladeFile file = new BladeFile(); + file.setOriginalName(originalName); + file.setName(key); + file.setDomain(getOssHost(bucketName)); + file.setLink(fileLink(bucketName, key)); + return file; + } + + @Override + @SneakyThrows + public void removeFile(String fileName) { + ossClient.deleteObject(getBucketName(), fileName); + } + + @Override + @SneakyThrows + public void removeFile(String bucketName, String fileName) { + ossClient.deleteObject(getBucketName(bucketName), fileName); + } + + @Override + @SneakyThrows + public void removeFiles(List fileNames) { + fileNames.forEach(this::removeFile); + } + + @Override + @SneakyThrows + public void removeFiles(String bucketName, List fileNames) { + fileNames.forEach(fileName -> removeFile(getBucketName(bucketName), fileName)); + } + + /** + * 获取私有存储文件输入流 + * + * @param fileName 存储桶文件名称 + * @return InputStream + */ + @Override + public InputStream statFileStream(String fileName) { + return statFileStream(ossProperties.getBucketName(), fileName); + } + + /** + * 获取私有存储文件输入流 + * + * @param bucketName 存储桶名称 + * @param fileName 存储桶文件名称 + * @return InputStream + */ + @Override + public InputStream statFileStream(String bucketName, String fileName) { + OSSObject object = ossClient.getObject(getBucketName(bucketName), fileName); + return object.getObjectContent(); + } + + /** + * 根据规则生成存储桶名称规则 + * + * @return String + */ + private String getBucketName() { + return getBucketName(ossProperties.getBucketName()); + } + + /** + * 根据规则生成存储桶名称规则 + * + * @param bucketName 存储桶名称 + * @return String + */ + private String getBucketName(String bucketName) { + return ossRule.bucketName(bucketName); + } + + /** + * 根据规则生成文件名称规则 + * + * @param originalFilename 原始文件名 + * @return string + */ + private String getFileName(String originalFilename) { + return ossRule.fileName(originalFilename); + } + + public String getUploadToken() { + return getUploadToken(ossProperties.getBucketName()); + } + + /** + * TODO 过期时间 + *

+ * 获取上传凭证,普通上传 + */ + public String getUploadToken(String bucketName) { + // 默认过期时间2小时 + return getUploadToken(bucketName, ossProperties.getArgs().get("expireTime", 3600L)); + } + + /** + * TODO 上传大小限制、基础路径 + *

+ * 获取上传凭证,普通上传 + */ + public String getUploadToken(String bucketName, long expireTime) { + String baseDir = "upload"; + + long expireEndTime = System.currentTimeMillis() + expireTime * 1000; + Date expiration = new Date(expireEndTime); + + PolicyConditions policyConds = new PolicyConditions(); + // 默认大小限制10M + policyConds.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, ossProperties.getArgs().get("contentLengthRange", 10485760)); + policyConds.addConditionItem(MatchMode.StartWith, PolicyConditions.COND_KEY, baseDir); + + String postPolicy = ossClient.generatePostPolicy(expiration, policyConds); + byte[] binaryData = postPolicy.getBytes(StandardCharsets.UTF_8); + String encodedPolicy = BinaryUtil.toBase64String(binaryData); + String postSignature = ossClient.calculatePostSignature(postPolicy); + + Map respMap = new LinkedHashMap<>(16); + respMap.put("accessid", ossProperties.getAccessKey()); + respMap.put("policy", encodedPolicy); + respMap.put("signature", postSignature); + respMap.put("dir", baseDir); + respMap.put("host", getOssHost(bucketName)); + respMap.put("expire", String.valueOf(expireEndTime / 1000)); + return JsonUtil.toJson(respMap); + } + + /** + * 获取域名 + * + * @param bucketName 存储桶名称 + * @return String + */ + public String getOssHost(String bucketName) { + String prefix = getEndpoint().contains("https://") ? "https://" : "http://"; + return prefix + getBucketName(bucketName) + StringPool.DOT + getEndpoint().replaceFirst(prefix, StringPool.EMPTY); + } + + /** + * 获取域名 + * + * @return String + */ + public String getOssHost() { + return getOssHost(ossProperties.getBucketName()); + } + + /** + * 获取服务地址 + * + * @return String + */ + public String getEndpoint() { + if (StringUtil.isBlank(ossProperties.getTransformEndpoint())) { + return ossProperties.getEndpoint(); + } + return ossProperties.getTransformEndpoint(); + } + +} diff --git a/blade-starter-oss/src/main/java/org/springblade/core/oss/HuaweiObsTemplate.java b/blade-starter-oss/src/main/java/org/springblade/core/oss/HuaweiObsTemplate.java new file mode 100644 index 0000000..45d65d7 --- /dev/null +++ b/blade-starter-oss/src/main/java/org/springblade/core/oss/HuaweiObsTemplate.java @@ -0,0 +1,264 @@ +package org.springblade.core.oss; + +import com.obs.services.ObsClient; +import com.obs.services.model.ObjectMetadata; +import com.obs.services.model.ObsObject; +import com.obs.services.model.PutObjectResult; +import lombok.AllArgsConstructor; +import lombok.SneakyThrows; +import org.springblade.core.oss.model.BladeFile; +import org.springblade.core.oss.model.OssFile; +import org.springblade.core.oss.props.OssProperties; +import org.springblade.core.oss.rule.OssRule; +import org.springblade.core.tool.utils.StringPool; +import org.springblade.core.tool.utils.StringUtil; +import org.springframework.util.StringUtils; +import org.springframework.web.multipart.MultipartFile; + +import java.io.InputStream; +import java.util.List; + +/** + * @author Tonny + */ +@AllArgsConstructor +public class HuaweiObsTemplate implements OssTemplate { + + private final ObsClient obsClient; + private final OssProperties ossProperties; + private final OssRule ossRule; + + @Override + public void makeBucket(String bucketName) { + if (!bucketExists(bucketName)) { + obsClient.createBucket(getBucketName(bucketName)); + } + } + + @Override + public void removeBucket(String bucketName) { + obsClient.deleteBucket(getBucketName(bucketName)); + } + + @Override + public boolean bucketExists(String bucketName) { + return obsClient.headBucket(getBucketName(bucketName)); + } + + @Override + public void copyFile(String bucketName, String fileName, String destBucketName) { + obsClient.copyObject(getBucketName(bucketName), fileName, getBucketName(destBucketName), fileName); + } + + @Override + public void copyFile(String bucketName, String fileName, String destBucketName, String destFileName) { + obsClient.copyObject(getBucketName(bucketName), fileName, getBucketName(destBucketName), destFileName); + } + + @Override + public OssFile statFile(String fileName) { + return statFile(ossProperties.getBucketName(), fileName); + } + + @Override + public OssFile statFile(String bucketName, String fileName) { + ObjectMetadata stat = obsClient.getObjectMetadata(getBucketName(bucketName), fileName); + OssFile ossFile = new OssFile(); + ossFile.setName(fileName); + ossFile.setLink(fileLink(ossFile.getName())); + ossFile.setHash(stat.getContentMd5()); + ossFile.setLength(stat.getContentLength()); + ossFile.setPutTime(stat.getLastModified()); + ossFile.setContentType(stat.getContentType()); + return ossFile; + } + + @Override + public String filePath(String fileName) { + return getOssHost(getBucketName()).concat(StringPool.SLASH).concat(fileName); + } + + @Override + public String filePath(String bucketName, String fileName) { + return getOssHost(getBucketName(bucketName)).concat(StringPool.SLASH).concat(fileName); + } + + @Override + public String fileLink(String fileName) { + return getOssHost().concat(StringPool.SLASH).concat(fileName); + } + + @Override + public String fileLink(String bucketName, String fileName) { + return getOssHost(getBucketName(bucketName)).concat(StringPool.SLASH).concat(fileName); + } + + @Override + public BladeFile putFile(MultipartFile file) { + return putFile(ossProperties.getBucketName(), file.getOriginalFilename(), file); + } + + @Override + public BladeFile putFile(String fileName, MultipartFile file) { + return putFile(ossProperties.getBucketName(), fileName, file); + } + + @Override + @SneakyThrows + public BladeFile putFile(String bucketName, String fileName, MultipartFile file) { + return putFile(bucketName, fileName, file.getInputStream()); + } + + @Override + public BladeFile putFile(String fileName, InputStream stream) { + return putFile(ossProperties.getBucketName(), fileName, stream); + } + + @Override + public BladeFile putFile(String bucketName, String fileName, InputStream stream) { + return put(bucketName, stream, fileName, false); + } + + @Override + public void removeFile(String fileName) { + obsClient.deleteObject(getBucketName(), fileName); + } + + @Override + public void removeFile(String bucketName, String fileName) { + obsClient.deleteObject(getBucketName(bucketName), fileName); + } + + @Override + public void removeFiles(List fileNames) { + fileNames.forEach(this::removeFile); + } + + @Override + public void removeFiles(String bucketName, List fileNames) { + fileNames.forEach(fileName -> removeFile(getBucketName(bucketName), fileName)); + } + + /** + * 获取私有存储文件输入流 + * + * @param fileName 存储桶文件名称 + * @return InputStream + */ + @Override + public InputStream statFileStream(String fileName) { + return statFileStream(ossProperties.getBucketName(),fileName); + } + + /** + * 获取私有存储文件输入流 + * + * @param bucketName 存储桶名称 + * @param fileName 存储桶文件名称 + * @return InputStream + */ + @Override + public InputStream statFileStream(String bucketName, String fileName) { + ObsObject object = obsClient.getObject(getBucketName(bucketName), fileName); + return object.getObjectContent(); + } + + /** + * 上传文件流 + * + * @param bucketName + * @param stream + * @param key + * @param cover + * @return + */ + @SneakyThrows + public BladeFile put(String bucketName, InputStream stream, String key, boolean cover) { + makeBucket(bucketName); + + String originalName = key; + + key = getFileName(key); + + // 覆盖上传 + if (cover) { + obsClient.putObject(getBucketName(bucketName), key, stream); + } else { + PutObjectResult response = obsClient.putObject(getBucketName(bucketName), key, stream); + int retry = 0; + int retryCount = 5; + while (StringUtils.isEmpty(response.getEtag()) && retry < retryCount) { + response = obsClient.putObject(getBucketName(bucketName), key, stream); + retry++; + } + } + + BladeFile file = new BladeFile(); + file.setOriginalName(originalName); + file.setName(key); + file.setDomain(getOssHost(bucketName)); + file.setLink(fileLink(bucketName, key)); + return file; + } + + /** + * 根据规则生成文件名称规则 + * + * @param originalFilename 原始文件名 + * @return string + */ + private String getFileName(String originalFilename) { + return ossRule.fileName(originalFilename); + } + + /** + * 根据规则生成存储桶名称规则 单租户 + * + * @return String + */ + private String getBucketName() { + return getBucketName(ossProperties.getBucketName()); + } + + /** + * 根据规则生成存储桶名称规则 多租户 + * + * @param bucketName 存储桶名称 + * @return String + */ + private String getBucketName(String bucketName) { + return ossRule.bucketName(bucketName); + } + + /** + * 获取域名 + * + * @param bucketName 存储桶名称 + * @return String + */ + public String getOssHost(String bucketName) { + String prefix = getEndpoint().contains("https://") ? "https://" : "http://"; + return prefix + getBucketName(bucketName) + StringPool.DOT + getEndpoint().replaceFirst(prefix, StringPool.EMPTY); + } + + /** + * 获取域名 + * + * @return String + */ + public String getOssHost() { + return getOssHost(ossProperties.getBucketName()); + } + + /** + * 获取服务地址 + * + * @return String + */ + public String getEndpoint() { + if (StringUtil.isBlank(ossProperties.getTransformEndpoint())) { + return ossProperties.getEndpoint(); + } + return ossProperties.getTransformEndpoint(); + } +} diff --git a/blade-starter-oss/src/main/java/org/springblade/core/oss/MinioTemplate.java b/blade-starter-oss/src/main/java/org/springblade/core/oss/MinioTemplate.java new file mode 100644 index 0000000..fd7c38e --- /dev/null +++ b/blade-starter-oss/src/main/java/org/springblade/core/oss/MinioTemplate.java @@ -0,0 +1,478 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oss; + +import io.minio.*; +import io.minio.http.Method; +import io.minio.messages.Bucket; +import io.minio.messages.DeleteObject; +import lombok.AllArgsConstructor; +import lombok.SneakyThrows; +import org.springblade.core.oss.enums.PolicyType; +import org.springblade.core.oss.model.BladeFile; +import org.springblade.core.oss.model.OssFile; +import org.springblade.core.oss.props.OssProperties; +import org.springblade.core.oss.rule.OssRule; +import org.springblade.core.tool.utils.DateUtil; +import org.springblade.core.tool.utils.Func; +import org.springblade.core.tool.utils.StringPool; +import org.springblade.core.tool.utils.StringUtil; +import org.springframework.web.multipart.MultipartFile; + +import java.io.InputStream; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +/** + * MinIOTemplate + * + * @author Chill + */ +@AllArgsConstructor +public class MinioTemplate implements OssTemplate { + + /** + * MinIO客户端 + */ + private final MinioClient client; + + /** + * 存储桶命名规则 + */ + private final OssRule ossRule; + + /** + * 配置类 + */ + private final OssProperties ossProperties; + + + @Override + @SneakyThrows + public void makeBucket(String bucketName) { + if ( + !client.bucketExists( + BucketExistsArgs.builder().bucket(getBucketName(bucketName)).build() + ) + ) { + client.makeBucket( + MakeBucketArgs.builder().bucket(getBucketName(bucketName)).build() + ); + client.setBucketPolicy( + SetBucketPolicyArgs.builder().bucket(getBucketName(bucketName)).config(getPolicyType(getBucketName(bucketName), PolicyType.READ)).build() + ); + } + } + + @SneakyThrows + public Bucket getBucket() { + return getBucket(getBucketName()); + } + + @SneakyThrows + public Bucket getBucket(String bucketName) { + Optional bucketOptional = client.listBuckets().stream().filter(bucket -> bucket.name().equals(getBucketName(bucketName))).findFirst(); + return bucketOptional.orElse(null); + } + + @SneakyThrows + public List listBuckets() { + return client.listBuckets(); + } + + @Override + @SneakyThrows + public void removeBucket(String bucketName) { + client.removeBucket( + RemoveBucketArgs.builder().bucket(getBucketName(bucketName)).build() + ); + } + + @Override + @SneakyThrows + public boolean bucketExists(String bucketName) { + return client.bucketExists( + BucketExistsArgs.builder().bucket(getBucketName(bucketName)).build() + ); + } + + @Override + @SneakyThrows + public void copyFile(String bucketName, String fileName, String destBucketName) { + copyFile(bucketName, fileName, destBucketName, fileName); + } + + @Override + @SneakyThrows + public void copyFile(String bucketName, String fileName, String destBucketName, String destFileName) { + client.copyObject( + CopyObjectArgs.builder() + .source(CopySource.builder().bucket(getBucketName(bucketName)).object(fileName).build()) + .bucket(getBucketName(destBucketName)) + .object(destFileName) + .build() + ); + } + + @Override + @SneakyThrows + public OssFile statFile(String fileName) { + return statFile(ossProperties.getBucketName(), fileName); + } + + @Override + @SneakyThrows + public OssFile statFile(String bucketName, String fileName) { + StatObjectResponse stat = client.statObject( + StatObjectArgs.builder().bucket(getBucketName(bucketName)).object(fileName).build() + ); + OssFile ossFile = new OssFile(); + ossFile.setName(Func.isEmpty(stat.object()) ? fileName : stat.object()); + ossFile.setLink(fileLink(ossFile.getName())); + ossFile.setHash(String.valueOf(stat.hashCode())); + ossFile.setLength(stat.size()); + ossFile.setPutTime(DateUtil.toDate(stat.lastModified().toLocalDateTime())); + ossFile.setContentType(stat.contentType()); + return ossFile; + } + + @Override + public String filePath(String fileName) { + return getBucketName().concat(StringPool.SLASH).concat(fileName); + } + + @Override + public String filePath(String bucketName, String fileName) { + return getBucketName(bucketName).concat(StringPool.SLASH).concat(fileName); + } + + @Override + @SneakyThrows + public String fileLink(String fileName) { + return getEndpoint().concat(StringPool.SLASH).concat(getBucketName()).concat(StringPool.SLASH).concat(fileName); + } + + @Override + @SneakyThrows + public String fileLink(String bucketName, String fileName) { + return getEndpoint().concat(StringPool.SLASH).concat(getBucketName(bucketName)).concat(StringPool.SLASH).concat(fileName); + } + + @Override + @SneakyThrows + public BladeFile putFile(MultipartFile file) { + return putFile(ossProperties.getBucketName(), file.getOriginalFilename(), file); + } + + @Override + @SneakyThrows + public BladeFile putFile(String fileName, MultipartFile file) { + return putFile(ossProperties.getBucketName(), fileName, file); + } + + @Override + @SneakyThrows + public BladeFile putFile(String bucketName, String fileName, MultipartFile file) { + return putFile(bucketName, file.getOriginalFilename(), file.getInputStream()); + } + + @Override + @SneakyThrows + public BladeFile putFile(String fileName, InputStream stream) { + return putFile(ossProperties.getBucketName(), fileName, stream); + } + + @Override + @SneakyThrows + public BladeFile putFile(String bucketName, String fileName, InputStream stream) { + return putFile(bucketName, fileName, stream, "application/octet-stream"); + } + + @SneakyThrows + public BladeFile putFile(String bucketName, String fileName, InputStream stream, String contentType) { + makeBucket(bucketName); + String originalName = fileName; + fileName = getFileName(fileName); + try { + client.putObject( + PutObjectArgs.builder() + .bucket(getBucketName(bucketName)) + .object(fileName) + .stream(stream, stream.available(), -1) + .contentType(contentType) + .build() + ); + } catch (Exception e) { + e.printStackTrace(); + } finally { + try { + if (stream != null) { + stream.close(); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + BladeFile file = new BladeFile(); + file.setOriginalName(originalName); + file.setName(fileName); + file.setDomain(getOssHost(bucketName)); + file.setLink(fileLink(bucketName, fileName)); + return file; + } + + @Override + @SneakyThrows + public void removeFile(String fileName) { + removeFile(ossProperties.getBucketName(), fileName); + } + + @Override + @SneakyThrows + public void removeFile(String bucketName, String fileName) { + client.removeObject( + RemoveObjectArgs.builder().bucket(getBucketName(bucketName)).object(fileName).build() + ); + } + + @Override + @SneakyThrows + public void removeFiles(List fileNames) { + removeFiles(ossProperties.getBucketName(), fileNames); + } + + @Override + @SneakyThrows + public void removeFiles(String bucketName, List fileNames) { + Stream stream = fileNames.stream().map(DeleteObject::new); + client.removeObjects(RemoveObjectsArgs.builder().bucket(getBucketName(bucketName)).objects(stream::iterator).build()); + } + + /** + * 获取私有存储文件输入流 + * + * @param fileName 存储桶文件名称 + * @return InputStream + */ + @Override + public InputStream statFileStream(String fileName) { + return statFileStream(ossProperties.getBucketName(), fileName); + } + + /** + * 获取私有存储文件输入流 + * + * @param bucketName 存储桶名称 + * @param fileName 存储桶文件名称 + * @return InputStream + */ + @Override + @SneakyThrows + public InputStream statFileStream(String bucketName, String fileName) { + return client.getObject( + GetObjectArgs.builder().bucket(getBucketName(bucketName)).object(fileName).build() + ); + } + + /** + * 根据规则生成存储桶名称规则 + * + * @return String + */ + private String getBucketName() { + return getBucketName(ossProperties.getBucketName()); + } + + /** + * 根据规则生成存储桶名称规则 + * + * @param bucketName 存储桶名称 + * @return String + */ + private String getBucketName(String bucketName) { + return ossRule.bucketName(bucketName); + } + + /** + * 根据规则生成文件名称规则 + * + * @param originalFilename 原始文件名 + * @return string + */ + private String getFileName(String originalFilename) { + return ossRule.fileName(originalFilename); + } + + /** + * 获取文件外链 + * + * @param bucketName bucket名称 + * @param fileName 文件名称 + * @param expires 过期时间 <=7 秒级 + * @return url + */ + @SneakyThrows + public String getPresignedObjectUrl(String bucketName, String fileName, Integer expires) { + return client.getPresignedObjectUrl( + GetPresignedObjectUrlArgs.builder() + .method(Method.GET) + .bucket(getBucketName(bucketName)) + .object(fileName) + .expiry(expires) + .build() + ); + } + + /** + * 获取存储桶策略 + * + * @param policyType 策略枚举 + * @return String + */ + public String getPolicyType(PolicyType policyType) { + return getPolicyType(getBucketName(), policyType); + } + + /** + * 获取存储桶策略 + * + * @param bucketName 存储桶名称 + * @param policyType 策略枚举 + * @return String + */ + public static String getPolicyType(String bucketName, PolicyType policyType) { + StringBuilder builder = new StringBuilder(); + builder.append("{\n"); + builder.append(" \"Statement\": [\n"); + builder.append(" {\n"); + builder.append(" \"Action\": [\n"); + + switch (policyType) { + case WRITE: + builder.append(" \"s3:GetBucketLocation\",\n"); + builder.append(" \"s3:ListBucketMultipartUploads\"\n"); + break; + case READ_WRITE: + builder.append(" \"s3:GetBucketLocation\",\n"); + builder.append(" \"s3:ListBucket\",\n"); + builder.append(" \"s3:ListBucketMultipartUploads\"\n"); + break; + default: + builder.append(" \"s3:GetBucketLocation\"\n"); + break; + } + + builder.append(" ],\n"); + builder.append(" \"Effect\": \"Allow\",\n"); + builder.append(" \"Principal\": \"*\",\n"); + builder.append(" \"Resource\": \"arn:aws:s3:::"); + builder.append(bucketName); + builder.append("\"\n"); + builder.append(" },\n"); + if (PolicyType.READ.equals(policyType)) { + builder.append(" {\n"); + builder.append(" \"Action\": [\n"); + builder.append(" \"s3:ListBucket\"\n"); + builder.append(" ],\n"); + builder.append(" \"Effect\": \"Deny\",\n"); + builder.append(" \"Principal\": \"*\",\n"); + builder.append(" \"Resource\": \"arn:aws:s3:::"); + builder.append(bucketName); + builder.append("\"\n"); + builder.append(" },\n"); + + } + builder.append(" {\n"); + builder.append(" \"Action\": "); + + switch (policyType) { + case WRITE: + builder.append("[\n"); + builder.append(" \"s3:AbortMultipartUpload\",\n"); + builder.append(" \"s3:DeleteObject\",\n"); + builder.append(" \"s3:ListMultipartUploadParts\",\n"); + builder.append(" \"s3:PutObject\"\n"); + builder.append(" ],\n"); + break; + case READ_WRITE: + builder.append("[\n"); + builder.append(" \"s3:AbortMultipartUpload\",\n"); + builder.append(" \"s3:DeleteObject\",\n"); + builder.append(" \"s3:GetObject\",\n"); + builder.append(" \"s3:ListMultipartUploadParts\",\n"); + builder.append(" \"s3:PutObject\"\n"); + builder.append(" ],\n"); + break; + default: + builder.append("\"s3:GetObject\",\n"); + break; + } + + builder.append(" \"Effect\": \"Allow\",\n"); + builder.append(" \"Principal\": \"*\",\n"); + builder.append(" \"Resource\": \"arn:aws:s3:::"); + builder.append(bucketName); + builder.append("/*\"\n"); + builder.append(" }\n"); + builder.append(" ],\n"); + builder.append(" \"Version\": \"2012-10-17\"\n"); + builder.append("}\n"); + return builder.toString(); + } + + /** + * 获取域名 + * + * @param bucketName 存储桶名称 + * @return String + */ + public String getOssHost(String bucketName) { + return getEndpoint() + StringPool.SLASH + getBucketName(bucketName); + } + + /** + * 获取域名 + * + * @return String + */ + public String getOssHost() { + return getOssHost(ossProperties.getBucketName()); + } + + /** + * 获取服务地址 + * + * @return String + */ + public String getEndpoint() { + if (StringUtil.isBlank(ossProperties.getTransformEndpoint())) { + return ossProperties.getEndpoint(); + } + return ossProperties.getTransformEndpoint(); + } + +} diff --git a/blade-starter-oss/src/main/java/org/springblade/core/oss/OssTemplate.java b/blade-starter-oss/src/main/java/org/springblade/core/oss/OssTemplate.java new file mode 100644 index 0000000..74e9911 --- /dev/null +++ b/blade-starter-oss/src/main/java/org/springblade/core/oss/OssTemplate.java @@ -0,0 +1,228 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oss; + + +import org.springblade.core.oss.model.BladeFile; +import org.springblade.core.oss.model.OssFile; +import org.springframework.web.multipart.MultipartFile; + +import java.io.InputStream; +import java.util.List; + +/** + * OssTemplate抽象API + * + * @author Chill + */ +public interface OssTemplate { + + /** + * 创建 存储桶 + * + * @param bucketName 存储桶名称 + */ + void makeBucket(String bucketName); + + /** + * 删除 存储桶 + * + * @param bucketName 存储桶名称 + */ + void removeBucket(String bucketName); + + /** + * 存储桶是否存在 + * + * @param bucketName 存储桶名称 + * @return boolean + */ + boolean bucketExists(String bucketName); + + /** + * 拷贝文件 + * + * @param bucketName 存储桶名称 + * @param fileName 存储桶文件名称 + * @param destBucketName 目标存储桶名称 + */ + void copyFile(String bucketName, String fileName, String destBucketName); + + /** + * 拷贝文件 + * + * @param bucketName 存储桶名称 + * @param fileName 存储桶文件名称 + * @param destBucketName 目标存储桶名称 + * @param destFileName 目标存储桶文件名称 + */ + void copyFile(String bucketName, String fileName, String destBucketName, String destFileName); + + /** + * 获取文件信息 + * + * @param fileName 存储桶文件名称 + * @return InputStream + */ + OssFile statFile(String fileName); + + /** + * 获取文件信息 + * + * @param bucketName 存储桶名称 + * @param fileName 存储桶文件名称 + * @return InputStream + */ + OssFile statFile(String bucketName, String fileName); + + /** + * 获取文件相对路径 + * + * @param fileName 存储桶对象名称 + * @return String + */ + String filePath(String fileName); + + /** + * 获取文件相对路径 + * + * @param bucketName 存储桶名称 + * @param fileName 存储桶对象名称 + * @return String + */ + String filePath(String bucketName, String fileName); + + /** + * 获取文件地址 + * + * @param fileName 存储桶对象名称 + * @return String + */ + String fileLink(String fileName); + + /** + * 获取文件地址 + * + * @param bucketName 存储桶名称 + * @param fileName 存储桶对象名称 + * @return String + */ + String fileLink(String bucketName, String fileName); + + /** + * 上传文件 + * + * @param file 上传文件类 + * @return BladeFile + */ + BladeFile putFile(MultipartFile file); + + /** + * 上传文件 + * + * @param file 上传文件类 + * @param fileName 上传文件名 + * @return BladeFile + */ + BladeFile putFile(String fileName, MultipartFile file); + + /** + * 上传文件 + * + * @param bucketName 存储桶名称 + * @param fileName 上传文件名 + * @param file 上传文件类 + * @return BladeFile + */ + BladeFile putFile(String bucketName, String fileName, MultipartFile file); + + /** + * 上传文件 + * + * @param fileName 存储桶对象名称 + * @param stream 文件流 + * @return BladeFile + */ + BladeFile putFile(String fileName, InputStream stream); + + /** + * 上传文件 + * + * @param bucketName 存储桶名称 + * @param fileName 存储桶对象名称 + * @param stream 文件流 + * @return BladeFile + */ + BladeFile putFile(String bucketName, String fileName, InputStream stream); + + /** + * 删除文件 + * + * @param fileName 存储桶对象名称 + */ + void removeFile(String fileName); + + /** + * 删除文件 + * + * @param bucketName 存储桶名称 + * @param fileName 存储桶对象名称 + */ + void removeFile(String bucketName, String fileName); + + /** + * 批量删除文件 + * + * @param fileNames 存储桶对象名称集合 + */ + void removeFiles(List fileNames); + + /** + * 批量删除文件 + * + * @param bucketName 存储桶名称 + * @param fileNames 存储桶对象名称集合 + */ + void removeFiles(String bucketName, List fileNames); + + + /** + * 获取私有存储文件输入流 + * + * @param fileName 存储桶文件名称 + * @return InputStream + */ + InputStream statFileStream(String fileName); + + /** + * 获取私有存储文件输入流 + * + * @param bucketName 存储桶名称 + * @param fileName 存储桶文件名称 + * @return InputStream + */ + InputStream statFileStream(String bucketName, String fileName); +} diff --git a/blade-starter-oss/src/main/java/org/springblade/core/oss/QiniuTemplate.java b/blade-starter-oss/src/main/java/org/springblade/core/oss/QiniuTemplate.java new file mode 100644 index 0000000..5d6049f --- /dev/null +++ b/blade-starter-oss/src/main/java/org/springblade/core/oss/QiniuTemplate.java @@ -0,0 +1,321 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oss; + +import com.qiniu.common.Zone; +import com.qiniu.http.Response; +import com.qiniu.storage.BucketManager; +import com.qiniu.storage.UploadManager; +import com.qiniu.storage.model.FileInfo; +import com.qiniu.util.Auth; +import lombok.AllArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.ResponseBody; +import org.springblade.core.oss.model.BladeFile; +import org.springblade.core.oss.model.OssFile; +import org.springblade.core.oss.props.OssProperties; +import org.springblade.core.oss.rule.OssRule; +import org.springblade.core.tool.utils.CollectionUtil; +import org.springblade.core.tool.utils.Func; +import org.springblade.core.tool.utils.StringPool; +import org.springblade.core.tool.utils.StringUtil; +import org.springframework.web.multipart.MultipartFile; + +import java.io.InputStream; +import java.util.Date; +import java.util.List; + +/** + * QiniuTemplate + * + * @author Chill + */ +@AllArgsConstructor +@Slf4j +public class QiniuTemplate implements OssTemplate { + private final Auth auth; + private final UploadManager uploadManager; + private final BucketManager bucketManager; + private final OssProperties ossProperties; + private final OssRule ossRule; + + @Override + @SneakyThrows + public void makeBucket(String bucketName) { + if (!CollectionUtil.contains(bucketManager.buckets(), getBucketName(bucketName))) { + bucketManager.createBucket(getBucketName(bucketName), Zone.autoZone().getRegion()); + } + } + + @Override + public void removeBucket(String bucketName) { + + } + + @Override + @SneakyThrows + public boolean bucketExists(String bucketName) { + return CollectionUtil.contains(bucketManager.buckets(), getBucketName(bucketName)); + } + + @Override + @SneakyThrows + public void copyFile(String bucketName, String fileName, String destBucketName) { + bucketManager.copy(getBucketName(bucketName), fileName, getBucketName(destBucketName), fileName); + } + + @Override + @SneakyThrows + public void copyFile(String bucketName, String fileName, String destBucketName, String destFileName) { + bucketManager.copy(getBucketName(bucketName), fileName, getBucketName(destBucketName), destFileName); + } + + @Override + @SneakyThrows + public OssFile statFile(String fileName) { + return statFile(ossProperties.getBucketName(), fileName); + } + + @Override + @SneakyThrows + public OssFile statFile(String bucketName, String fileName) { + FileInfo stat = bucketManager.stat(getBucketName(bucketName), fileName); + OssFile ossFile = new OssFile(); + ossFile.setName(Func.isEmpty(stat.key) ? fileName : stat.key); + ossFile.setLink(fileLink(ossFile.getName())); + ossFile.setHash(stat.hash); + ossFile.setLength(stat.fsize); + ossFile.setPutTime(new Date(stat.putTime / 10000)); + ossFile.setContentType(stat.mimeType); + return ossFile; + } + + @Override + @SneakyThrows + public String filePath(String fileName) { + return getBucketName().concat(StringPool.SLASH).concat(fileName); + } + + @Override + @SneakyThrows + public String filePath(String bucketName, String fileName) { + return getBucketName(bucketName).concat(StringPool.SLASH).concat(fileName); + } + + @Override + @SneakyThrows + public String fileLink(String fileName) { + return getEndpoint().concat(StringPool.SLASH).concat(fileName); + } + + @Override + @SneakyThrows + public String fileLink(String bucketName, String fileName) { + return getEndpoint().concat(StringPool.SLASH).concat(fileName); + } + + @Override + @SneakyThrows + public BladeFile putFile(MultipartFile file) { + return putFile(ossProperties.getBucketName(), file.getOriginalFilename(), file); + } + + @Override + @SneakyThrows + public BladeFile putFile(String fileName, MultipartFile file) { + return putFile(ossProperties.getBucketName(), fileName, file); + } + + @Override + @SneakyThrows + public BladeFile putFile(String bucketName, String fileName, MultipartFile file) { + return putFile(bucketName, fileName, file.getInputStream()); + } + + @Override + @SneakyThrows + public BladeFile putFile(String fileName, InputStream stream) { + return putFile(ossProperties.getBucketName(), fileName, stream); + } + + @Override + @SneakyThrows + public BladeFile putFile(String bucketName, String fileName, InputStream stream) { + return put(bucketName, stream, fileName, false); + } + + @SneakyThrows + public BladeFile put(String bucketName, InputStream stream, String key, boolean cover) { + makeBucket(bucketName); + String originalName = key; + key = getFileName(key); + // 覆盖上传 + if (cover) { + uploadManager.put(stream, key, getUploadToken(bucketName, key), null, null); + } else { + Response response = uploadManager.put(stream, key, getUploadToken(bucketName), null, null); + int retry = 0; + int retryCount = 5; + while (response.needRetry() && retry < retryCount) { + response = uploadManager.put(stream, key, getUploadToken(bucketName), null, null); + retry++; + } + } + BladeFile file = new BladeFile(); + file.setOriginalName(originalName); + file.setName(key); + file.setDomain(getOssHost()); + file.setLink(fileLink(bucketName, key)); + return file; + } + + @Override + @SneakyThrows + public void removeFile(String fileName) { + bucketManager.delete(getBucketName(), fileName); + } + + @Override + @SneakyThrows + public void removeFile(String bucketName, String fileName) { + bucketManager.delete(getBucketName(bucketName), fileName); + } + + @Override + @SneakyThrows + public void removeFiles(List fileNames) { + fileNames.forEach(this::removeFile); + } + + @Override + @SneakyThrows + public void removeFiles(String bucketName, List fileNames) { + fileNames.forEach(fileName -> removeFile(getBucketName(bucketName), fileName)); + } + + /** + * 获取私有存储文件输入流 + * + * @param fileName 存储桶文件名称 + * @return InputStream + */ + @Override + public InputStream statFileStream(String fileName) { + return statFileStream(ossProperties.getBucketName(), fileName); + } + + /** + * 获取文件信息 + * + * @param bucketName 存储桶名称 + * @param fileName 存储桶文件名称 + * @return InputStream + */ + @Override + @SneakyThrows + public InputStream statFileStream(String bucketName, String fileName) { + String downloadUrl = auth.privateDownloadUrl(fileLink(fileName)); + OkHttpClient client = new OkHttpClient(); + Request req = new Request.Builder().url(downloadUrl).build(); + okhttp3.Response resp = null; + resp = client.newCall(req).execute(); + if (resp.isSuccessful()) { + ResponseBody body = resp.body(); + return body.byteStream(); + } + log.info("当前文件下载失败{}请检查文件是否存在或者文件是否为私有文件",fileName); + return null; + } + + /** + * 根据规则生成存储桶名称规则 + * + * @return String + */ + private String getBucketName() { + return getBucketName(ossProperties.getBucketName()); + } + + /** + * 根据规则生成存储桶名称规则 + * + * @param bucketName 存储桶名称 + * @return String + */ + private String getBucketName(String bucketName) { + return ossRule.bucketName(bucketName); + } + + /** + * 根据规则生成文件名称规则 + * + * @param originalFilename 原始文件名 + * @return string + */ + private String getFileName(String originalFilename) { + return ossRule.fileName(originalFilename); + } + + /** + * 获取上传凭证,普通上传 + */ + public String getUploadToken(String bucketName) { + return auth.uploadToken(getBucketName(bucketName)); + } + + /** + * 获取上传凭证,覆盖上传 + */ + private String getUploadToken(String bucketName, String key) { + return auth.uploadToken(getBucketName(bucketName), key); + } + + + /** + * 获取域名 + * + * @return String + */ + public String getOssHost() { + return getEndpoint(); + } + + /** + * 获取服务地址 + * + * @return String + */ + public String getEndpoint() { + if (StringUtil.isBlank(ossProperties.getTransformEndpoint())) { + return ossProperties.getEndpoint(); + } + return ossProperties.getTransformEndpoint(); + } + +} diff --git a/blade-starter-oss/src/main/java/org/springblade/core/oss/S3Template.java b/blade-starter-oss/src/main/java/org/springblade/core/oss/S3Template.java new file mode 100644 index 0000000..3892d33 --- /dev/null +++ b/blade-starter-oss/src/main/java/org/springblade/core/oss/S3Template.java @@ -0,0 +1,534 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oss; + +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.*; +import lombok.AllArgsConstructor; +import lombok.SneakyThrows; +import org.springblade.core.oss.enums.PolicyType; +import org.springblade.core.oss.model.BladeFile; +import org.springblade.core.oss.model.OssFile; +import org.springblade.core.oss.props.OssProperties; +import org.springblade.core.oss.rule.OssRule; +import org.springblade.core.tool.utils.Func; +import org.springblade.core.tool.utils.StringPool; +import org.springblade.core.tool.utils.StringUtil; +import org.springframework.web.multipart.MultipartFile; + +import java.io.InputStream; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * S3Template + * + * @author fanjia2 + * @version 1.0.0 + * @ClassName S3Template.java + * @Description Aws {@link S3Template 的代码实现} + * @createTime 2022年07月04日 11:17:00 + */ +@AllArgsConstructor +public class S3Template implements OssTemplate { + + + /** + * S3客户端 + */ + private final AmazonS3 client; + + /** + * 存储桶命名规则 + */ + private final OssRule ossRule; + + /** + * 配置类 + */ + private final OssProperties ossProperties; + + /** + * 创建 存储桶 + * + * @param bucketName 存储桶名称 + */ + @Override + public void makeBucket(String bucketName) { + if ( + !client.doesBucketExistV2( + getBucketName(bucketName) + ) + ) { + client.createBucket( + getBucketName(bucketName)); + client.setBucketPolicy(new SetBucketPolicyRequest(getBucketName(bucketName), getPolicyType(getBucketName(bucketName), PolicyType.READ))); + } + } + + @SneakyThrows + public Bucket getBucket() { + return getBucket(getBucketName()); + } + + @SneakyThrows + public Bucket getBucket(String bucketName) { + Optional bucketOptional = client.listBuckets().stream().filter(bucket -> bucket.getName().equals(getBucketName(bucketName))).findFirst(); + return bucketOptional.orElse(null); + } + + @SneakyThrows + public List listBuckets() { + return client.listBuckets(); + } + + /** + * 删除 存储桶 + * + * @param bucketName 存储桶名称 + */ + @Override + public void removeBucket(String bucketName) { + client.deleteBucket(new DeleteBucketRequest(getBucketName(bucketName))); + } + + /** + * 存储桶是否存在 + * + * @param bucketName 存储桶名称 + * @return boolean + */ + @Override + public boolean bucketExists(String bucketName) { + return client.doesBucketExistV2( + getBucketName(bucketName)); + } + + /** + * 拷贝文件 + * + * @param bucketName 存储桶名称 + * @param fileName 存储桶文件名称 + * @param destBucketName 目标存储桶名称 + */ + @Override + public void copyFile(String bucketName, String fileName, String destBucketName) { + copyFile(bucketName, fileName, destBucketName, fileName); + + } + + /** + * 拷贝文件 + * + * @param bucketName 存储桶名称 + * @param fileName 存储桶文件名称 + * @param destBucketName 目标存储桶名称 + * @param destFileName 目标存储桶文件名称 + */ + @Override + public void copyFile(String bucketName, String fileName, String destBucketName, String destFileName) { + + client.copyObject(new CopyObjectRequest(getBucketName(bucketName), fileName, getBucketName(destBucketName), destFileName)); + + } + + /** + * 获取文件信息 + * + * @param fileName 存储桶文件名称 + * @return InputStream + */ + @Override + public OssFile statFile(String fileName) { + return statFile(ossProperties.getBucketName(), fileName); + } + + /** + * 获取文件信息 + * + * @param bucketName 存储桶名称 + * @param fileName 存储桶文件名称 + * @return InputStream + */ + @Override + public OssFile statFile(String bucketName, String fileName) { + S3Object stat = client.getObject(new GetObjectRequest(getBucketName(bucketName), fileName)); + OssFile ossFile = new OssFile(); + ossFile.setName(Func.isEmpty(stat.getKey()) ? fileName : stat.getKey()); + ossFile.setLink(fileLink(ossFile.getName())); + ossFile.setHash(String.valueOf(stat.hashCode())); + ossFile.setLength(stat.getObjectMetadata().getContentLength()); + ossFile.setPutTime(stat.getObjectMetadata().getLastModified()); + ossFile.setContentType(stat.getObjectMetadata().getContentType()); + return ossFile; + } + + /** + * 获取文件信息 + * + * @param fileName 存储桶文件名称 + * @return InputStream + */ + @Override + public InputStream statFileStream(String fileName) { + return statFileStream(getBucketName(), fileName); + } + + /** + * 获取文件信息 + * + * @param bucketName 存储桶名称 + * @param fileName 存储桶文件名称 + * @return InputStream + */ + @Override + public InputStream statFileStream(String bucketName, String fileName) { + return client.getObject(new GetObjectRequest(getBucketName(bucketName), fileName)).getObjectContent(); + } + + /** + * 获取文件相对路径 + * + * @param fileName 存储桶对象名称 + * @return String + */ + @Override + public String filePath(String fileName) { + return getBucketName().concat(StringPool.SLASH).concat(fileName); + } + + /** + * 获取文件相对路径 + * + * @param bucketName 存储桶名称 + * @param fileName 存储桶对象名称 + * @return String + */ + @Override + public String filePath(String bucketName, String fileName) { + return getBucketName(bucketName).concat(StringPool.SLASH).concat(fileName); + } + + /** + * 获取文件地址 + * + * @param fileName 存储桶对象名称 + * @return String + */ + @Override + @SneakyThrows + public String fileLink(String fileName) { + return getEndpoint().concat(StringPool.SLASH).concat(getBucketName()).concat(StringPool.SLASH).concat(fileName); + } + + /** + * 获取文件地址 + * + * @param bucketName 存储桶名称 + * @param fileName 存储桶对象名称 + * @return String + */ + @Override + @SneakyThrows + public String fileLink(String bucketName, String fileName) { + return getEndpoint().concat(StringPool.SLASH).concat(getBucketName(bucketName)).concat(StringPool.SLASH).concat(fileName); + } + + /** + * 上传文件 + * + * @param file 上传文件类 + * @return BladeFile + */ + @Override + @SneakyThrows + public BladeFile putFile(MultipartFile file) { + return putFile(ossProperties.getBucketName(), file.getOriginalFilename(), file); + } + + /** + * 上传文件 + * + * @param fileName 上传文件名 + * @param file 上传文件类 + * @return BladeFile + */ + @Override + @SneakyThrows + public BladeFile putFile(String fileName, MultipartFile file) { + return putFile(ossProperties.getBucketName(), fileName, file); + } + + /** + * 上传文件 + * + * @param bucketName 存储桶名称 + * @param fileName 上传文件名 + * @param file 上传文件类 + * @return BladeFile + */ + @Override + @SneakyThrows + public BladeFile putFile(String bucketName, String fileName, MultipartFile file) { + return putFile(bucketName, file.getOriginalFilename(), file.getInputStream()); + } + + /** + * 上传文件 + * + * @param fileName 存储桶对象名称 + * @param stream 文件流 + * @return BladeFile + */ + @Override + public BladeFile putFile(String fileName, InputStream stream) { + return putFile(ossProperties.getBucketName(), fileName, stream); + } + + /** + * 上传文件 + * + * @param bucketName 存储桶名称 + * @param fileName 存储桶对象名称 + * @param stream 文件流 + * @return BladeFile + */ + @Override + public BladeFile putFile(String bucketName, String fileName, InputStream stream) { + return putFile(bucketName, fileName, stream, "application/octet-stream"); + + } + + @SneakyThrows + public BladeFile putFile(String bucketName, String fileName, InputStream stream, String contentType) { + makeBucket(bucketName); + String originalName = fileName; + ObjectMetadata objectMetadata = new ObjectMetadata(); + objectMetadata.setContentType(contentType); + fileName = getFileName(fileName); + client.putObject(new PutObjectRequest(getBucketName(bucketName), fileName, stream, objectMetadata)); + BladeFile file = new BladeFile(); + file.setOriginalName(originalName); + file.setName(fileName); + file.setDomain(getOssHost(bucketName)); + file.setLink(fileLink(bucketName, fileName)); + return file; + } + + /** + * 删除文件 + * + * @param fileName 存储桶对象名称 + */ + @Override + public void removeFile(String fileName) { + + removeFile(ossProperties.getBucketName(), fileName); + } + + /** + * 删除文件 + * + * @param bucketName 存储桶名称 + * @param fileName 存储桶对象名称 + */ + @Override + public void removeFile(String bucketName, String fileName) { + client.deleteObject(bucketName, fileName); + } + + /** + * 批量删除文件 + * + * @param fileNames 存储桶对象名称集合 + */ + @Override + public void removeFiles(List fileNames) { + + removeFiles(ossProperties.getBucketName(), fileNames); + } + + /** + * 批量删除文件 + * + * @param bucketName 存储桶名称 + * @param fileNames 存储桶对象名称集合 + */ + @Override + public void removeFiles(String bucketName, List fileNames) { + List keyVersions = fileNames.stream().map(DeleteObjectsRequest.KeyVersion::new).collect(Collectors.toList()); + DeleteObjectsRequest deleteObjectsRequest = new DeleteObjectsRequest(getBucketName()); + client.deleteObjects(deleteObjectsRequest.withKeys(keyVersions)); + } + + /** + * 根据规则生成存储桶名称规则 + * + * @return String + */ + private String getBucketName() { + return getBucketName(ossProperties.getBucketName()); + } + + /** + * 根据规则生成存储桶名称规则 + * + * @param bucketName 存储桶名称 + * @return String + */ + private String getBucketName(String bucketName) { + return ossRule.bucketName(bucketName); + } + + /** + * 根据规则生成文件名称规则 + * + * @param originalFilename 原始文件名 + * @return string + */ + private String getFileName(String originalFilename) { + return ossRule.fileName(originalFilename); + } + + /** + * 获取存储桶策略 + * + * @param bucketName 存储桶名称 + * @param policyType 策略枚举 + * @return String + */ + public static String getPolicyType(String bucketName, PolicyType policyType) { + StringBuilder builder = new StringBuilder(); + builder.append("{\n"); + builder.append(" \"Statement\": [\n"); + builder.append(" {\n"); + builder.append(" \"Action\": [\n"); + + switch (policyType) { + case WRITE: + builder.append(" \"s3:GetBucketLocation\",\n"); + builder.append(" \"s3:ListBucketMultipartUploads\"\n"); + break; + case READ_WRITE: + builder.append(" \"s3:GetBucketLocation\",\n"); + builder.append(" \"s3:ListBucket\",\n"); + builder.append(" \"s3:ListBucketMultipartUploads\"\n"); + break; + default: + builder.append(" \"s3:GetBucketLocation\"\n"); + break; + } + + builder.append(" ],\n"); + builder.append(" \"Effect\": \"Allow\",\n"); + builder.append(" \"Principal\": \"*\",\n"); + builder.append(" \"Resource\": \"arn:aws:s3:::"); + builder.append(bucketName); + builder.append("\"\n"); + builder.append(" },\n"); + if (PolicyType.READ.equals(policyType)) { + builder.append(" {\n"); + builder.append(" \"Action\": [\n"); + builder.append(" \"s3:ListBucket\"\n"); + builder.append(" ],\n"); + builder.append(" \"Effect\": \"Deny\",\n"); + builder.append(" \"Principal\": \"*\",\n"); + builder.append(" \"Resource\": \"arn:aws:s3:::"); + builder.append(bucketName); + builder.append("\"\n"); + builder.append(" },\n"); + + } + builder.append(" {\n"); + builder.append(" \"Action\": "); + + switch (policyType) { + case WRITE: + builder.append("[\n"); + builder.append(" \"s3:AbortMultipartUpload\",\n"); + builder.append(" \"s3:DeleteObject\",\n"); + builder.append(" \"s3:ListMultipartUploadParts\",\n"); + builder.append(" \"s3:PutObject\"\n"); + builder.append(" ],\n"); + break; + case READ_WRITE: + builder.append("[\n"); + builder.append(" \"s3:AbortMultipartUpload\",\n"); + builder.append(" \"s3:DeleteObject\",\n"); + builder.append(" \"s3:GetObject\",\n"); + builder.append(" \"s3:ListMultipartUploadParts\",\n"); + builder.append(" \"s3:PutObject\"\n"); + builder.append(" ],\n"); + break; + default: + builder.append("\"s3:GetObject\",\n"); + break; + } + + builder.append(" \"Effect\": \"Allow\",\n"); + builder.append(" \"Principal\": \"*\",\n"); + builder.append(" \"Resource\": \"arn:aws:s3:::"); + builder.append(bucketName); + builder.append("/*\"\n"); + builder.append(" }\n"); + builder.append(" ],\n"); + builder.append(" \"Version\": \"2012-10-17\"\n"); + builder.append("}\n"); + return builder.toString(); + } + + /** + * 获取域名 + * + * @param bucketName 存储桶名称 + * @return String + */ + public String getOssHost(String bucketName) { + return getEndpoint() + StringPool.SLASH + getBucketName(bucketName); + } + + /** + * 获取域名 + * + * @return String + */ + public String getOssHost() { + return getOssHost(ossProperties.getBucketName()); + } + + /** + * 获取服务地址 + * + * @return String + */ + public String getEndpoint() { + if (StringUtil.isBlank(ossProperties.getTransformEndpoint())) { + return ossProperties.getEndpoint(); + } + return ossProperties.getTransformEndpoint(); + } +} diff --git a/blade-starter-oss/src/main/java/org/springblade/core/oss/TencentCosTemplate.java b/blade-starter-oss/src/main/java/org/springblade/core/oss/TencentCosTemplate.java new file mode 100644 index 0000000..9f6ef75 --- /dev/null +++ b/blade-starter-oss/src/main/java/org/springblade/core/oss/TencentCosTemplate.java @@ -0,0 +1,315 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oss; + +import com.qcloud.cos.COSClient; +import com.qcloud.cos.model.COSObject; +import com.qcloud.cos.model.CannedAccessControlList; +import com.qcloud.cos.model.ObjectMetadata; +import com.qcloud.cos.model.PutObjectResult; +import lombok.AllArgsConstructor; +import lombok.SneakyThrows; +import org.springblade.core.oss.model.BladeFile; +import org.springblade.core.oss.model.OssFile; +import org.springblade.core.oss.props.OssProperties; +import org.springblade.core.oss.rule.OssRule; +import org.springblade.core.tool.utils.StringPool; +import org.springblade.core.tool.utils.StringUtil; +import org.springframework.util.StringUtils; +import org.springframework.web.multipart.MultipartFile; + +import java.io.InputStream; +import java.util.List; + +/** + *

+ * 腾讯云 COS 操作 + *

+ * + * @author yangkai.shen + * @date Created in 2020/1/7 17:24 + */ +@AllArgsConstructor +public class TencentCosTemplate implements OssTemplate { + private final COSClient cosClient; + private final OssProperties ossProperties; + private final OssRule ossRule; + + @Override + @SneakyThrows + public void makeBucket(String bucketName) { + if (!bucketExists(bucketName)) { + cosClient.createBucket(getBucketName(bucketName)); + // TODO: 权限是否需要修改为私有,当前为 公有读、私有写 + cosClient.setBucketAcl(getBucketName(bucketName), CannedAccessControlList.PublicRead); + } + } + + @Override + @SneakyThrows + public void removeBucket(String bucketName) { + cosClient.deleteBucket(getBucketName(bucketName)); + } + + @Override + @SneakyThrows + public boolean bucketExists(String bucketName) { + return cosClient.doesBucketExist(getBucketName(bucketName)); + } + + @Override + @SneakyThrows + public void copyFile(String bucketName, String fileName, String destBucketName) { + cosClient.copyObject(getBucketName(bucketName), fileName, getBucketName(destBucketName), fileName); + } + + @Override + @SneakyThrows + public void copyFile(String bucketName, String fileName, String destBucketName, String destFileName) { + cosClient.copyObject(getBucketName(bucketName), fileName, getBucketName(destBucketName), destFileName); + } + + @Override + @SneakyThrows + public OssFile statFile(String fileName) { + return statFile(ossProperties.getBucketName(), fileName); + } + + @Override + @SneakyThrows + public OssFile statFile(String bucketName, String fileName) { + ObjectMetadata stat = cosClient.getObjectMetadata(getBucketName(bucketName), fileName); + OssFile ossFile = new OssFile(); + ossFile.setName(fileName); + ossFile.setLink(fileLink(ossFile.getName())); + ossFile.setHash(stat.getContentMD5()); + ossFile.setLength(stat.getContentLength()); + ossFile.setPutTime(stat.getLastModified()); + ossFile.setContentType(stat.getContentType()); + return ossFile; + } + + @Override + @SneakyThrows + public String filePath(String fileName) { + return getOssHost().concat(StringPool.SLASH).concat(fileName); + } + + @Override + @SneakyThrows + public String filePath(String bucketName, String fileName) { + return getOssHost(bucketName).concat(StringPool.SLASH).concat(fileName); + } + + @Override + @SneakyThrows + public String fileLink(String fileName) { + return getOssHost().concat(StringPool.SLASH).concat(fileName); + } + + @Override + @SneakyThrows + public String fileLink(String bucketName, String fileName) { + return getOssHost(bucketName).concat(StringPool.SLASH).concat(fileName); + } + + /** + * 文件对象 + * + * @param file 上传文件类 + * @return + */ + @Override + @SneakyThrows + public BladeFile putFile(MultipartFile file) { + return putFile(ossProperties.getBucketName(), file.getOriginalFilename(), file); + } + + /** + * @param fileName 上传文件名 + * @param file 上传文件类 + * @return + */ + @Override + @SneakyThrows + public BladeFile putFile(String fileName, MultipartFile file) { + return putFile(ossProperties.getBucketName(), fileName, file); + } + + @Override + @SneakyThrows + public BladeFile putFile(String bucketName, String fileName, MultipartFile file) { + return putFile(bucketName, fileName, file.getInputStream()); + } + + @Override + @SneakyThrows + public BladeFile putFile(String fileName, InputStream stream) { + return putFile(ossProperties.getBucketName(), fileName, stream); + } + + @Override + @SneakyThrows + public BladeFile putFile(String bucketName, String fileName, InputStream stream) { + return put(bucketName, stream, fileName, false); + } + + @SneakyThrows + public BladeFile put(String bucketName, InputStream stream, String key, boolean cover) { + makeBucket(bucketName); + String originalName = key; + key = getFileName(key); + ObjectMetadata objectMetadata = new ObjectMetadata(); + // 覆盖上传 + if (cover) { + cosClient.putObject(getBucketName(bucketName), key, stream, objectMetadata); + } else { + PutObjectResult response = cosClient.putObject(getBucketName(bucketName), key, stream, objectMetadata); + int retry = 0; + int retryCount = 5; + while (!StringUtils.hasText(response.getETag()) && retry < retryCount) { + response = cosClient.putObject(getBucketName(bucketName), key, stream, objectMetadata); + retry++; + } + } + BladeFile file = new BladeFile(); + file.setOriginalName(originalName); + file.setName(key); + file.setDomain(getOssHost(bucketName)); + file.setLink(fileLink(bucketName, key)); + return file; + } + + @Override + @SneakyThrows + public void removeFile(String fileName) { + cosClient.deleteObject(getBucketName(), fileName); + } + + @Override + @SneakyThrows + public void removeFile(String bucketName, String fileName) { + cosClient.deleteObject(getBucketName(bucketName), fileName); + } + + @Override + @SneakyThrows + public void removeFiles(List fileNames) { + fileNames.forEach(this::removeFile); + } + + @Override + @SneakyThrows + public void removeFiles(String bucketName, List fileNames) { + fileNames.forEach(fileName -> removeFile(getBucketName(bucketName), fileName)); + } + + /** + * 获取私有存储文件输入流 + * + * @param fileName 存储桶文件名称 + * @return InputStream + */ + @Override + public InputStream statFileStream(String fileName) { + return statFileStream(ossProperties.getBucketName(), fileName); + } + + /** + * 获取私有存储文件输入流 + * + * @param bucketName 存储桶名称 + * @param fileName 存储桶文件名称 + * @return InputStream + */ + @Override + public InputStream statFileStream(String bucketName, String fileName) { + COSObject object = cosClient.getObject(getBucketName(bucketName), fileName); + return object.getObjectContent(); + } + + /** + * 根据规则生成存储桶名称规则 + * + * @return String + */ + private String getBucketName() { + return getBucketName(ossProperties.getBucketName()); + } + + /** + * 根据规则生成存储桶名称规则 + * + * @param bucketName 存储桶名称 + * @return String + */ + private String getBucketName(String bucketName) { + return ossRule.bucketName(bucketName).concat(StringPool.DASH).concat(ossProperties.getAppId()); + } + + /** + * 根据规则生成文件名称规则 + * + * @param originalFilename 原始文件名 + * @return string + */ + private String getFileName(String originalFilename) { + return ossRule.fileName(originalFilename); + } + + /** + * 获取域名 + * + * @param bucketName 存储桶名称 + * @return String + */ + public String getOssHost(String bucketName) { + String prefix = getEndpoint().contains("https://") ? "https://" : "http://"; + return prefix + cosClient.getClientConfig().getEndpointBuilder().buildGeneralApiEndpoint(getBucketName(bucketName)); + } + + /** + * 获取域名 + * + * @return String + */ + public String getOssHost() { + return getOssHost(ossProperties.getBucketName()); + } + + /** + * 获取服务地址 + * + * @return String + */ + public String getEndpoint() { + if (StringUtil.isBlank(ossProperties.getTransformEndpoint())) { + return ossProperties.getEndpoint(); + } + return ossProperties.getTransformEndpoint(); + } + +} diff --git a/blade-starter-oss/src/main/java/org/springblade/core/oss/config/AliossConfiguration.java b/blade-starter-oss/src/main/java/org/springblade/core/oss/config/AliossConfiguration.java new file mode 100644 index 0000000..ab03933 --- /dev/null +++ b/blade-starter-oss/src/main/java/org/springblade/core/oss/config/AliossConfiguration.java @@ -0,0 +1,87 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oss.config; + +import com.aliyun.oss.ClientConfiguration; +import com.aliyun.oss.OSSClient; +import com.aliyun.oss.common.auth.CredentialsProvider; +import com.aliyun.oss.common.auth.DefaultCredentialProvider; +import lombok.AllArgsConstructor; +import org.springblade.core.oss.AliossTemplate; +import org.springblade.core.oss.props.OssProperties; +import org.springblade.core.oss.rule.OssRule; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; + +/** + * Alioss配置类 + * + * @author Chill + */ +@AllArgsConstructor +@AutoConfiguration(after = OssConfiguration.class) +@EnableConfigurationProperties(OssProperties.class) +@ConditionalOnClass({OSSClient.class}) +@ConditionalOnProperty(value = "oss.name", havingValue = "alioss") +public class AliossConfiguration { + + private final OssProperties ossProperties; + private final OssRule ossRule; + + @Bean + @ConditionalOnMissingBean(OSSClient.class) + public OSSClient ossClient() { + // 创建ClientConfiguration。ClientConfiguration是OSSClient的配置类,可配置代理、连接超时、最大连接数等参数。 + ClientConfiguration conf = new ClientConfiguration(); + // 设置OSSClient允许打开的最大HTTP连接数,默认为1024个。 + conf.setMaxConnections(1024); + // 设置Socket层传输数据的超时时间,默认为50000毫秒。 + conf.setSocketTimeout(50000); + // 设置建立连接的超时时间,默认为50000毫秒。 + conf.setConnectionTimeout(50000); + // 设置从连接池中获取连接的超时时间(单位:毫秒),默认不超时。 + conf.setConnectionRequestTimeout(1000); + // 设置连接空闲超时时间。超时则关闭连接,默认为60000毫秒。 + conf.setIdleConnectionTime(60000); + // 设置失败请求重试次数,默认为3次。 + conf.setMaxErrorRetry(5); + CredentialsProvider credentialsProvider = new DefaultCredentialProvider(ossProperties.getAccessKey(), ossProperties.getSecretKey()); + return new OSSClient(ossProperties.getEndpoint(), credentialsProvider, conf); + } + + @Bean + @ConditionalOnBean({OSSClient.class}) + @ConditionalOnMissingBean(AliossTemplate.class) + public AliossTemplate aliossTemplate(OSSClient ossClient) { + return new AliossTemplate(ossClient, ossProperties, ossRule); + } + +} diff --git a/blade-starter-oss/src/main/java/org/springblade/core/oss/config/HuaweiObsConfiguration.java b/blade-starter-oss/src/main/java/org/springblade/core/oss/config/HuaweiObsConfiguration.java new file mode 100644 index 0000000..55799ae --- /dev/null +++ b/blade-starter-oss/src/main/java/org/springblade/core/oss/config/HuaweiObsConfiguration.java @@ -0,0 +1,65 @@ +package org.springblade.core.oss.config; + +import com.aliyun.oss.OSSClient; +import com.obs.services.ObsClient; +import com.obs.services.ObsConfiguration; +import lombok.AllArgsConstructor; +import org.springblade.core.oss.HuaweiObsTemplate; +import org.springblade.core.oss.props.OssProperties; +import org.springblade.core.oss.rule.BladeOssRule; +import org.springblade.core.oss.rule.OssRule; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; + +/** + * @author Tonny + */ +@AllArgsConstructor +@AutoConfiguration(after = OssConfiguration.class) +@EnableConfigurationProperties(OssProperties.class) +@ConditionalOnClass({OSSClient.class}) +@ConditionalOnProperty(value = "oss.name", havingValue = "huaweiobs") +public class HuaweiObsConfiguration { + private final OssProperties ossProperties; + + @Bean + @ConditionalOnMissingBean(OssRule.class) + public OssRule ossRule() { + return new BladeOssRule(ossProperties.getTenantMode()); + } + + @Bean + @ConditionalOnMissingBean(ObsClient.class) + public ObsClient ossClient() { + // 使用可定制各参数的配置类(ObsConfiguration)创建OBS客户端(ObsClient),创建完成后不支持再次修改参数 + ObsConfiguration conf = new ObsConfiguration (); + + conf.setEndPoint(ossProperties.getEndpoint()); + // 设置OSSClient允许打开的最大HTTP连接数,默认为1024个。 + conf.setMaxConnections(1024); + // 设置Socket层传输数据的超时时间,默认为50000毫秒。 + conf.setSocketTimeout(50000); + // 设置建立连接的超时时间,默认为50000毫秒。 + conf.setConnectionTimeout(50000); + // 设置从连接池中获取连接的超时时间(单位:毫秒),默认不超时。 + conf.setConnectionRequestTimeout(1000); + // 设置连接空闲超时时间。超时则关闭连接,默认为60000毫秒。 + conf.setIdleConnectionTime(60000); + // 设置失败请求重试次数,默认为3次。 + conf.setMaxErrorRetry(5); + + return new ObsClient(ossProperties.getAccessKey(), ossProperties.getSecretKey(), conf); + } + + @Bean + @ConditionalOnMissingBean(HuaweiObsTemplate.class) + @ConditionalOnBean({ObsClient.class, OssRule.class}) + public HuaweiObsTemplate huaweiobsTemplate(ObsClient obsClient, OssRule ossRule) { + return new HuaweiObsTemplate(obsClient, ossProperties, ossRule); + } +} diff --git a/blade-starter-oss/src/main/java/org/springblade/core/oss/config/MinioConfiguration.java b/blade-starter-oss/src/main/java/org/springblade/core/oss/config/MinioConfiguration.java new file mode 100644 index 0000000..b71d97a --- /dev/null +++ b/blade-starter-oss/src/main/java/org/springblade/core/oss/config/MinioConfiguration.java @@ -0,0 +1,75 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oss.config; + +import io.minio.MinioClient; +import lombok.AllArgsConstructor; +import lombok.SneakyThrows; +import org.springblade.core.oss.MinioTemplate; +import org.springblade.core.oss.props.OssProperties; +import org.springblade.core.oss.rule.OssRule; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; + +/** + * Minio配置类 + * + * @author Chill + */ +@AllArgsConstructor +@AutoConfiguration(after = OssConfiguration.class) +@ConditionalOnClass({MinioClient.class}) +@EnableConfigurationProperties(OssProperties.class) +@ConditionalOnProperty(value = "oss.name", havingValue = "minio") +public class MinioConfiguration { + + private final OssProperties ossProperties; + private final OssRule ossRule; + + + @Bean + @SneakyThrows + @ConditionalOnMissingBean(MinioClient.class) + public MinioClient minioClient() { + return MinioClient.builder() + .endpoint(ossProperties.getEndpoint()) + .credentials(ossProperties.getAccessKey(), ossProperties.getSecretKey()) + .build(); + } + + @Bean + @ConditionalOnBean({MinioClient.class}) + @ConditionalOnMissingBean(MinioTemplate.class) + public MinioTemplate minioTemplate(MinioClient minioClient) { + return new MinioTemplate(minioClient, ossRule, ossProperties); + } + +} diff --git a/blade-starter-oss/src/main/java/org/springblade/core/oss/config/OssConfiguration.java b/blade-starter-oss/src/main/java/org/springblade/core/oss/config/OssConfiguration.java new file mode 100644 index 0000000..dc670af --- /dev/null +++ b/blade-starter-oss/src/main/java/org/springblade/core/oss/config/OssConfiguration.java @@ -0,0 +1,55 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oss.config; + +import lombok.AllArgsConstructor; +import org.springblade.core.oss.props.OssProperties; +import org.springblade.core.oss.rule.BladeOssRule; +import org.springblade.core.oss.rule.OssRule; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; + +/** + * Oss配置类 + * + * @author Chill + */ +@AutoConfiguration +@AllArgsConstructor +@EnableConfigurationProperties(OssProperties.class) +public class OssConfiguration { + + private final OssProperties ossProperties; + + @Bean + @ConditionalOnMissingBean(OssRule.class) + public OssRule ossRule() { + return new BladeOssRule(ossProperties.getTenantMode()); + } + +} diff --git a/blade-starter-oss/src/main/java/org/springblade/core/oss/config/QiniuConfiguration.java b/blade-starter-oss/src/main/java/org/springblade/core/oss/config/QiniuConfiguration.java new file mode 100644 index 0000000..130137e --- /dev/null +++ b/blade-starter-oss/src/main/java/org/springblade/core/oss/config/QiniuConfiguration.java @@ -0,0 +1,118 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oss.config; + +import com.qiniu.storage.BucketManager; +import com.qiniu.storage.Region; +import com.qiniu.storage.UploadManager; +import com.qiniu.util.Auth; +import lombok.AllArgsConstructor; +import org.springblade.core.oss.QiniuTemplate; +import org.springblade.core.oss.props.OssProperties; +import org.springblade.core.oss.rule.OssRule; +import org.springblade.core.tool.utils.StringUtil; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; + +/** + * Qiniu配置类 + * + * @author Chill + */ +@AllArgsConstructor +@AutoConfiguration(after = OssConfiguration.class) +@ConditionalOnClass({Auth.class, UploadManager.class, BucketManager.class}) +@EnableConfigurationProperties(OssProperties.class) +@ConditionalOnProperty(value = "oss.name", havingValue = "qiniu") +public class QiniuConfiguration { + + private final OssProperties ossProperties; + private final OssRule ossRule; + + /** + * 增加七牛云配置region的方式 目前仅支持huadong huanan huabei beimei xinjiapo + * 默认不配置采用原有的方式实现 支持配置对应的region + */ + @Bean + @ConditionalOnMissingBean(com.qiniu.storage.Configuration.class) + public com.qiniu.storage.Configuration qnConfiguration() { + Region regin = Region.autoRegion(); + if (StringUtil.isNoneBlank(ossProperties.getRegion())) { + switch (ossProperties.getRegion()) { + case "huadong": + regin = Region.huadong(); + break; + case "huabei": + regin = Region.huabei(); + break; + case "huanan": + regin = Region.huanan(); + break; + case "beimei": + regin = Region.beimei(); + break; + case "xinjiapo": + regin = Region.xinjiapo(); + break; + default: + regin = Region.autoRegion(); + break; + } + } + return new com.qiniu.storage.Configuration(regin); + } + + @Bean + @ConditionalOnMissingBean(Auth.class) + public Auth auth() { + return Auth.create(ossProperties.getAccessKey(), ossProperties.getSecretKey()); + } + + @Bean + @ConditionalOnBean(com.qiniu.storage.Configuration.class) + public UploadManager uploadManager(com.qiniu.storage.Configuration cfg) { + return new UploadManager(cfg); + } + + @Bean + @ConditionalOnBean(com.qiniu.storage.Configuration.class) + public BucketManager bucketManager(com.qiniu.storage.Configuration cfg) { + return new BucketManager(Auth.create(ossProperties.getAccessKey(), ossProperties.getSecretKey()), cfg); + } + + @Bean + @ConditionalOnBean({Auth.class, UploadManager.class, BucketManager.class}) + @ConditionalOnMissingBean(QiniuTemplate.class) + public QiniuTemplate qiniuTemplate(Auth auth, UploadManager uploadManager, BucketManager bucketManager) { + return new QiniuTemplate(auth, uploadManager, bucketManager, ossProperties, ossRule); + } + +} diff --git a/blade-starter-oss/src/main/java/org/springblade/core/oss/config/S3Configuration.java b/blade-starter-oss/src/main/java/org/springblade/core/oss/config/S3Configuration.java new file mode 100644 index 0000000..450ff15 --- /dev/null +++ b/blade-starter-oss/src/main/java/org/springblade/core/oss/config/S3Configuration.java @@ -0,0 +1,97 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oss.config; + +import com.amazonaws.ClientConfiguration; +import com.amazonaws.auth.AWSCredentials; +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.client.builder.AwsClientBuilder; +import com.amazonaws.regions.Regions; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import lombok.AllArgsConstructor; +import lombok.SneakyThrows; +import org.springblade.core.oss.S3Template; +import org.springblade.core.oss.props.OssProperties; +import org.springblade.core.oss.rule.OssRule; +import org.springblade.core.tool.utils.StringUtil; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * S3Configuration + * + * @author fanjia2 + * @version 1.0.0 + * @ClassName S3Configuration.java + * @Description 亚马逊 S3 配置类 + * @createTime 2022年07月04日 11:19:00 + */ +@Configuration(proxyBeanMethods = false) +@AllArgsConstructor +@AutoConfiguration(after = OssConfiguration.class) +@ConditionalOnClass({AmazonS3.class}) +@EnableConfigurationProperties(OssProperties.class) +@ConditionalOnProperty(value = "oss.name", havingValue = "s3") +public class S3Configuration { + + + private final OssProperties ossProperties; + private final OssRule ossRule; + + + @Bean + @SneakyThrows + @ConditionalOnMissingBean(AmazonS3.class) + public AmazonS3 amazonS3() { + AWSCredentials credentials = new BasicAWSCredentials(ossProperties.getAccessKey(), ossProperties.getSecretKey()); + ClientConfiguration clientConfiguration = new ClientConfiguration(); + clientConfiguration.setSignerOverride("AWSS3V4SignerType"); + return AmazonS3ClientBuilder + .standard() + .withEndpointConfiguration(new AwsClientBuilder. + EndpointConfiguration(ossProperties.getEndpoint(), + StringUtil.isBlank(ossProperties.getRegion()) ? Regions.DEFAULT_REGION.name() : Regions.fromName(ossProperties.getRegion()).getName())) + .withPathStyleAccessEnabled(true) + .withClientConfiguration(clientConfiguration) + .withCredentials(new AWSStaticCredentialsProvider(credentials)) + .build(); + } + + @Bean + @ConditionalOnBean({AmazonS3.class}) + @ConditionalOnMissingBean(S3Template.class) + public S3Template s3Template(AmazonS3 amazonS3) { + return new S3Template(amazonS3, ossRule, ossProperties); + } +} diff --git a/blade-starter-oss/src/main/java/org/springblade/core/oss/config/TencentCosConfiguration.java b/blade-starter-oss/src/main/java/org/springblade/core/oss/config/TencentCosConfiguration.java new file mode 100644 index 0000000..71c3bcb --- /dev/null +++ b/blade-starter-oss/src/main/java/org/springblade/core/oss/config/TencentCosConfiguration.java @@ -0,0 +1,91 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oss.config; + +import com.qcloud.cos.COSClient; +import com.qcloud.cos.ClientConfig; +import com.qcloud.cos.auth.BasicCOSCredentials; +import com.qcloud.cos.auth.COSCredentials; +import com.qcloud.cos.region.Region; +import lombok.AllArgsConstructor; +import org.springblade.core.oss.TencentCosTemplate; +import org.springblade.core.oss.props.OssProperties; +import org.springblade.core.oss.rule.OssRule; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; + +/** + *

+ * 腾讯云 COS 自动装配 + *

+ * + * @author yangkai.shen + * @date Created in 2020/1/7 17:24 + */ +@AllArgsConstructor +@AutoConfiguration(after = OssConfiguration.class) +@ConditionalOnClass({COSClient.class}) +@EnableConfigurationProperties(OssProperties.class) +@ConditionalOnProperty(value = "oss.name", havingValue = "tencentcos") +public class TencentCosConfiguration { + + private final OssProperties ossProperties; + private final OssRule ossRule; + + + @Bean + @ConditionalOnMissingBean(COSClient.class) + public COSClient ossClient() { + // 初始化用户身份信息(secretId, secretKey) + COSCredentials credentials = new BasicCOSCredentials(ossProperties.getAccessKey(), ossProperties.getSecretKey()); + // 设置 bucket 的区域, COS 地域的简称请参照 https://cloud.tencent.com/document/product/436/6224 + Region region = new Region(ossProperties.getRegion()); + // clientConfig 中包含了设置 region, https(默认 http), 超时, 代理等 set 方法, 使用可参见源码或者常见问题 Java SDK 部分。 + ClientConfig clientConfig = new ClientConfig(region); + // 设置OSSClient允许打开的最大HTTP连接数,默认为1024个。 + clientConfig.setMaxConnectionsCount(1024); + // 设置Socket层传输数据的超时时间,默认为50000毫秒。 + clientConfig.setSocketTimeout(50000); + // 设置建立连接的超时时间,默认为50000毫秒。 + clientConfig.setConnectionTimeout(50000); + // 设置从连接池中获取连接的超时时间(单位:毫秒),默认不超时。 + clientConfig.setConnectionRequestTimeout(1000); + return new COSClient(credentials, clientConfig); + } + + @Bean + @ConditionalOnBean({COSClient.class}) + @ConditionalOnMissingBean(TencentCosTemplate.class) + public TencentCosTemplate tencentCosTemplate(COSClient cosClient) { + return new TencentCosTemplate(cosClient, ossProperties, ossRule); + } + +} diff --git a/blade-starter-oss/src/main/java/org/springblade/core/oss/enums/OssEnum.java b/blade-starter-oss/src/main/java/org/springblade/core/oss/enums/OssEnum.java new file mode 100644 index 0000000..4b4f716 --- /dev/null +++ b/blade-starter-oss/src/main/java/org/springblade/core/oss/enums/OssEnum.java @@ -0,0 +1,98 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oss.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * Oss枚举类 + * + * @author Chill + */ +@Getter +@AllArgsConstructor +public enum OssEnum { + + /** + * minio + */ + MINIO("minio", 1), + + /** + * qiniu + */ + QINIU("qiniu", 2), + + /** + * ali + */ + ALI("alioss", 3), + + /** + * tencent + */ + TENCENT("tencent", 4), + + /** + * huawei + */ + HUAWEI("huawei", 5), + + /** + * amazons3 + */ + AMAZONS3("amazon s3", 6); + + /** + * 名称 + */ + final String name; + /** + * 类型 + */ + final int category; + + /** + * 匹配枚举值 + * + * @param name 名称 + * @return OssEnum + */ + public static OssEnum of(String name) { + if (name == null) { + return null; + } + OssEnum[] values = OssEnum.values(); + for (OssEnum ossEnum : values) { + if (ossEnum.name.equals(name)) { + return ossEnum; + } + } + return null; + } + +} diff --git a/blade-starter-oss/src/main/java/org/springblade/core/oss/enums/OssStatusEnum.java b/blade-starter-oss/src/main/java/org/springblade/core/oss/enums/OssStatusEnum.java new file mode 100644 index 0000000..054ebb8 --- /dev/null +++ b/blade-starter-oss/src/main/java/org/springblade/core/oss/enums/OssStatusEnum.java @@ -0,0 +1,55 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oss.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * Oss类型枚举 + * + * @author Chill + */ +@Getter +@AllArgsConstructor +public enum OssStatusEnum { + + /** + * 关闭 + */ + DISABLE(1), + /** + * 启用 + */ + ENABLE(2), + ; + + /** + * 类型编号 + */ + final int num; + +} diff --git a/blade-starter-oss/src/main/java/org/springblade/core/oss/enums/PolicyType.java b/blade-starter-oss/src/main/java/org/springblade/core/oss/enums/PolicyType.java new file mode 100644 index 0000000..1af6cf1 --- /dev/null +++ b/blade-starter-oss/src/main/java/org/springblade/core/oss/enums/PolicyType.java @@ -0,0 +1,64 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oss.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * minio策略配置 + * + * @author SCMOX + */ +@Getter +@AllArgsConstructor +public enum PolicyType { + + /** + * 只读 + */ + READ("read", "只读"), + + /** + * 只写 + */ + WRITE("write", "只写"), + + /** + * 读写 + */ + READ_WRITE("read_write", "读写"); + + /** + * 类型 + */ + private final String type; + /** + * 描述 + */ + private final String policy; + +} diff --git a/blade-starter-oss/src/main/java/org/springblade/core/oss/model/BladeFile.java b/blade-starter-oss/src/main/java/org/springblade/core/oss/model/BladeFile.java new file mode 100644 index 0000000..bd7e413 --- /dev/null +++ b/blade-starter-oss/src/main/java/org/springblade/core/oss/model/BladeFile.java @@ -0,0 +1,57 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oss.model; + +import lombok.Data; + +/** + * BladeFile + * + * @author Chill + */ +@Data +public class BladeFile { + /** + * 文件地址 + */ + private String link; + /** + * 域名地址 + */ + private String domain; + /** + * 文件名 + */ + private String name; + /** + * 初始文件名 + */ + private String originalName; + /** + * 附件表ID + */ + private Long attachId; +} diff --git a/blade-starter-oss/src/main/java/org/springblade/core/oss/model/MinioItem.java b/blade-starter-oss/src/main/java/org/springblade/core/oss/model/MinioItem.java new file mode 100644 index 0000000..09415bd --- /dev/null +++ b/blade-starter-oss/src/main/java/org/springblade/core/oss/model/MinioItem.java @@ -0,0 +1,63 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oss.model; + +import io.minio.messages.Item; +import io.minio.messages.Owner; +import lombok.Data; +import org.springblade.core.tool.utils.DateUtil; + +import java.util.Date; + +/** + * MinioItem + * + * @author Chill + */ +@Data +public class MinioItem { + + private String objectName; + private Date lastModified; + private String etag; + private Long size; + private String storageClass; + private Owner owner; + private boolean isDir; + private String category; + + public MinioItem(Item item) { + this.objectName = item.objectName(); + this.lastModified = DateUtil.toDate(item.lastModified().toLocalDateTime()); + this.etag = item.etag(); + this.size = item.size(); + this.storageClass = item.storageClass(); + this.owner = item.owner(); + this.isDir = item.isDir(); + this.category = isDir ? "dir" : "file"; + } + +} diff --git a/blade-starter-oss/src/main/java/org/springblade/core/oss/model/OssFile.java b/blade-starter-oss/src/main/java/org/springblade/core/oss/model/OssFile.java new file mode 100644 index 0000000..9db8653 --- /dev/null +++ b/blade-starter-oss/src/main/java/org/springblade/core/oss/model/OssFile.java @@ -0,0 +1,63 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oss.model; + +import lombok.Data; + +import java.util.Date; + +/** + * OssFile + * + * @author Chill + */ +@Data +public class OssFile { + /** + * 文件地址 + */ + private String link; + /** + * 文件名 + */ + private String name; + /** + * 文件hash值 + */ + public String hash; + /** + * 文件大小 + */ + private long length; + /** + * 文件上传时间 + */ + private Date putTime; + /** + * 文件contentType + */ + private String contentType; +} diff --git a/blade-starter-oss/src/main/java/org/springblade/core/oss/props/OssProperties.java b/blade-starter-oss/src/main/java/org/springblade/core/oss/props/OssProperties.java new file mode 100644 index 0000000..8bcc31a --- /dev/null +++ b/blade-starter-oss/src/main/java/org/springblade/core/oss/props/OssProperties.java @@ -0,0 +1,96 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oss.props; + +import lombok.Data; +import org.springblade.core.tool.support.Kv; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Minio参数配置类 + * + * @author Chill + */ +@Data +@ConfigurationProperties(prefix = "oss") +public class OssProperties { + + /** + * 是否启用 + */ + private Boolean enabled; + + /** + * 对象存储名称 + */ + private String name; + + /** + * 是否开启租户模式 + */ + private Boolean tenantMode = false; + + /** + * 对象存储服务的URL + */ + private String endpoint; + + /** + * 转换外网地址的URL + */ + private String transformEndpoint; + + /** + * 应用ID TencentCOS需要 + */ + private String appId; + + /** + * 区域简称 TencentCOS/Amazon S3 需要 + */ + private String region; + + /** + * Access key就像用户ID,可以唯一标识你的账户 + */ + private String accessKey; + + /** + * Secret key是你账户的密码 + */ + private String secretKey; + + /** + * 默认的存储桶名称 + */ + private String bucketName = "bladex"; + + /** + * 自定义属性 + */ + private Kv args; + +} diff --git a/blade-starter-oss/src/main/java/org/springblade/core/oss/rule/BladeOssRule.java b/blade-starter-oss/src/main/java/org/springblade/core/oss/rule/BladeOssRule.java new file mode 100644 index 0000000..c601842 --- /dev/null +++ b/blade-starter-oss/src/main/java/org/springblade/core/oss/rule/BladeOssRule.java @@ -0,0 +1,59 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oss.rule; + +import lombok.AllArgsConstructor; +import org.springblade.core.secure.utils.AuthUtil; +import org.springblade.core.tool.utils.DateUtil; +import org.springblade.core.tool.utils.FileUtil; +import org.springblade.core.tool.utils.StringPool; +import org.springblade.core.tool.utils.StringUtil; + +/** + * 默认存储桶生成规则 + * + * @author Chill + */ +@AllArgsConstructor +public class BladeOssRule implements OssRule { + + /** + * 租户模式 + */ + private final Boolean tenantMode; + + @Override + public String bucketName(String bucketName) { + String prefix = (tenantMode) ? AuthUtil.getTenantId().concat(StringPool.DASH) : StringPool.EMPTY; + return prefix + bucketName; + } + + @Override + public String fileName(String originalFilename) { + return "upload" + StringPool.SLASH + DateUtil.today() + StringPool.SLASH + StringUtil.randomUUID() + StringPool.DOT + FileUtil.getFileExtension(originalFilename); + } + +} diff --git a/blade-starter-oss/src/main/java/org/springblade/core/oss/rule/OssRule.java b/blade-starter-oss/src/main/java/org/springblade/core/oss/rule/OssRule.java new file mode 100644 index 0000000..78ee6fd --- /dev/null +++ b/blade-starter-oss/src/main/java/org/springblade/core/oss/rule/OssRule.java @@ -0,0 +1,51 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.oss.rule; + +/** + * Oss通用规则 + * + * @author Chill + */ +public interface OssRule { + + /** + * 获取存储桶规则 + * + * @param bucketName 存储桶名称 + * @return String + */ + String bucketName(String bucketName); + + /** + * 获取文件名规则 + * + * @param originalFilename 文件名 + * @return String + */ + String fileName(String originalFilename); + +} diff --git a/blade-starter-powerjob/pom.xml b/blade-starter-powerjob/pom.xml new file mode 100644 index 0000000..2510487 --- /dev/null +++ b/blade-starter-powerjob/pom.xml @@ -0,0 +1,40 @@ + + + 4.0.0 + + org.springblade + BladeX-Tool + ${revision} + + + blade-starter-powerjob + ${project.artifactId} + ${project.parent.version} + jar + + + + + org.springblade + blade-core-launch + + + + tech.powerjob + powerjob-worker-spring-boot-starter + + + tech.powerjob + powerjob-client + + + + org.springblade + blade-core-auto + provided + + + + diff --git a/blade-starter-powerjob/src/main/java/org/springblade/core/powerjob/config/PowerJobConfiguration.java b/blade-starter-powerjob/src/main/java/org/springblade/core/powerjob/config/PowerJobConfiguration.java new file mode 100644 index 0000000..99d74bc --- /dev/null +++ b/blade-starter-powerjob/src/main/java/org/springblade/core/powerjob/config/PowerJobConfiguration.java @@ -0,0 +1,42 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.powerjob.config; + +import org.springblade.core.launch.props.BladePropertySource; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +/** + * powerjob 配置 + * + * @author Chill + */ +@AutoConfiguration +@BladePropertySource(value = "classpath:/blade-powerjob.yml") +public class PowerJobConfiguration implements WebMvcConfigurer { + +} + diff --git a/blade-starter-powerjob/src/main/java/org/springblade/core/powerjob/constant/PowerJobConstant.java b/blade-starter-powerjob/src/main/java/org/springblade/core/powerjob/constant/PowerJobConstant.java new file mode 100644 index 0000000..0f944b3 --- /dev/null +++ b/blade-starter-powerjob/src/main/java/org/springblade/core/powerjob/constant/PowerJobConstant.java @@ -0,0 +1,55 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.powerjob.constant; + +/** + * PowerJob常量 + * + * @author Chill + */ +public interface PowerJobConstant { + + /** + * 启用任务 + */ + int JOB_ENABLED = 1; + + /** + * 禁用任务 + */ + int JOB_DISABLED = 0; + + /** + * 删除状态 + */ + int JOB_DELETED = 99; + + /** + * 警告文本 + */ + String JOB_SYNC_ALERT = "请先同步任务数据"; + +} diff --git a/blade-starter-powerjob/src/main/resources/blade-powerjob.yml b/blade-starter-powerjob/src/main/resources/blade-powerjob.yml new file mode 100644 index 0000000..8526ed7 --- /dev/null +++ b/blade-starter-powerjob/src/main/resources/blade-powerjob.yml @@ -0,0 +1,11 @@ +powerjob: + worker: + app-name: ${spring.application.name} + port: 27777 + server-address: 127.0.0.1:7700 + protocol: http + store-strategy: disk + max-result-length: 4096 + max-appended-wf-context-length: 4096 + max-lightweight-task-num: 1024 + max-heavyweight-task-num: 64 diff --git a/blade-starter-prometheus/pom.xml b/blade-starter-prometheus/pom.xml new file mode 100644 index 0000000..762d0cd --- /dev/null +++ b/blade-starter-prometheus/pom.xml @@ -0,0 +1,57 @@ + + + 4.0.0 + + BladeX-Tool + org.springblade + ${revision} + + + blade-starter-prometheus + ${project.artifactId} + ${project.parent.version} + jar + + + + org.springframework.boot + spring-boot-starter-webflux + + + org.springframework.cloud + spring-cloud-commons + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + com.alibaba.nacos + nacos-client + + + + + com.alibaba.nacos + nacos-client + + + + org.springblade + blade-core-launch + + + org.springblade + blade-starter-metrics + + + + org.springblade + blade-core-auto + provided + + + + diff --git a/blade-starter-prometheus/src/main/java/org/springblade/core/prometheus/config/PrometheusConfiguration.java b/blade-starter-prometheus/src/main/java/org/springblade/core/prometheus/config/PrometheusConfiguration.java new file mode 100644 index 0000000..020d1ca --- /dev/null +++ b/blade-starter-prometheus/src/main/java/org/springblade/core/prometheus/config/PrometheusConfiguration.java @@ -0,0 +1,105 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.prometheus.config; + +import com.alibaba.cloud.nacos.NacosDiscoveryProperties; +import org.springblade.core.launch.props.BladePropertySource; +import org.springblade.core.prometheus.endpoint.AgentEndpoint; +import org.springblade.core.prometheus.endpoint.PrometheusApi; +import org.springblade.core.prometheus.endpoint.ReactivePrometheusApi; +import org.springblade.core.prometheus.endpoint.ServiceEndpoint; +import org.springblade.core.prometheus.service.RegistrationService; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cloud.client.ConditionalOnDiscoveryEnabled; +import org.springframework.cloud.client.ConditionalOnReactiveDiscoveryEnabled; +import org.springframework.cloud.client.discovery.DiscoveryClient; +import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Bean; +import org.springframework.core.env.Environment; + +/** + * Prometheus配置类 + * + * @author L.cm + */ +@AutoConfiguration +@BladePropertySource(value = "classpath:/blade-prometheus.yml") +public class PrometheusConfiguration { + + @Bean + public RegistrationService registrationService(DiscoveryClient discoveryClient) { + return new RegistrationService(discoveryClient); + } + + @Bean + public AgentEndpoint agentController(NacosDiscoveryProperties properties) { + return new AgentEndpoint(properties); + } + + @Bean + public ServiceEndpoint serviceController(RegistrationService registrationService) { + return new ServiceEndpoint(registrationService); + } + + @AutoConfiguration + @ConditionalOnBean(DiscoveryClient.class) + @ConditionalOnDiscoveryEnabled + // @ConditionalOnBlockingDiscoveryEnabled + @ConditionalOnProperty(value = "spring.cloud.discovery.blocking.enabled") + public static class PrometheusApiConfiguration { + + @Bean + public PrometheusApi prometheusApi(Environment environment, + DiscoveryClient discoveryClient, + ApplicationEventPublisher eventPublisher) { + String[] activeProfiles = environment.getActiveProfiles(); + String activeProfile = activeProfiles.length > 0 ? activeProfiles[0] : null; + return new PrometheusApi(activeProfile, discoveryClient, eventPublisher); + } + + } + + @AutoConfiguration + @ConditionalOnBean(ReactiveDiscoveryClient.class) + @ConditionalOnDiscoveryEnabled + @ConditionalOnReactiveDiscoveryEnabled + public static class ReactivePrometheusApiConfiguration { + + @Bean + public ReactivePrometheusApi reactivePrometheusApi(Environment environment, + ReactiveDiscoveryClient discoveryClient, + ApplicationEventPublisher eventPublisher) { + String[] activeProfiles = environment.getActiveProfiles(); + String activeProfile = activeProfiles.length > 0 ? activeProfiles[0] : null; + return new ReactivePrometheusApi(activeProfile, discoveryClient, eventPublisher); + } + + } + +} diff --git a/blade-starter-prometheus/src/main/java/org/springblade/core/prometheus/data/Agent.java b/blade-starter-prometheus/src/main/java/org/springblade/core/prometheus/data/Agent.java new file mode 100644 index 0000000..c862263 --- /dev/null +++ b/blade-starter-prometheus/src/main/java/org/springblade/core/prometheus/data/Agent.java @@ -0,0 +1,44 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.prometheus.data; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Getter; + +/** + * Agent + * + * @author L.cm + */ +@Getter +@Builder +public class Agent { + + @JsonProperty("Config") + private Config config; + +} diff --git a/blade-starter-prometheus/src/main/java/org/springblade/core/prometheus/data/ChangeItem.java b/blade-starter-prometheus/src/main/java/org/springblade/core/prometheus/data/ChangeItem.java new file mode 100644 index 0000000..fe5f273 --- /dev/null +++ b/blade-starter-prometheus/src/main/java/org/springblade/core/prometheus/data/ChangeItem.java @@ -0,0 +1,42 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.prometheus.data; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + + +/** + * ChangeItem + * + * @author L.cm + */ +@Getter +@RequiredArgsConstructor +public class ChangeItem { + private final T item; + private final long changeIndex; +} diff --git a/blade-starter-prometheus/src/main/java/org/springblade/core/prometheus/data/Config.java b/blade-starter-prometheus/src/main/java/org/springblade/core/prometheus/data/Config.java new file mode 100644 index 0000000..f3fd63d --- /dev/null +++ b/blade-starter-prometheus/src/main/java/org/springblade/core/prometheus/data/Config.java @@ -0,0 +1,44 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.prometheus.data; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Getter; + +/** + * Config + * + * @author L.cm + */ +@Getter +@Builder +public class Config { + + @JsonProperty("Datacenter") + private String dataCenter; + +} diff --git a/blade-starter-prometheus/src/main/java/org/springblade/core/prometheus/data/Service.java b/blade-starter-prometheus/src/main/java/org/springblade/core/prometheus/data/Service.java new file mode 100644 index 0000000..18e28df --- /dev/null +++ b/blade-starter-prometheus/src/main/java/org/springblade/core/prometheus/data/Service.java @@ -0,0 +1,49 @@ +package org.springblade.core.prometheus.data; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Getter; + +import java.util.List; +import java.util.Map; + +/** + * model details see https://www.consul.io/api/catalog.html#serviceport + * + * @author consul + */ +@Getter +@Builder +public class Service { + + @JsonProperty("Address") + private String address; + + @JsonProperty("Node") + private String node; + + @JsonProperty("ServiceAddress") + private String serviceAddress; + + @JsonProperty("ServiceName") + private String serviceName; + + @JsonProperty("ServiceID") + private String serviceId; + + @JsonProperty("ServicePort") + private int servicePort; + + @JsonProperty("NodeMeta") + private Map nodeMeta; + + @JsonProperty("ServiceMeta") + private Map serviceMeta; + + /** + * will be empty, eureka does not have the concept of service tags + */ + @JsonProperty("ServiceTags") + private List serviceTags; + +} diff --git a/blade-starter-prometheus/src/main/java/org/springblade/core/prometheus/data/ServiceHealth.java b/blade-starter-prometheus/src/main/java/org/springblade/core/prometheus/data/ServiceHealth.java new file mode 100644 index 0000000..6cbf611 --- /dev/null +++ b/blade-starter-prometheus/src/main/java/org/springblade/core/prometheus/data/ServiceHealth.java @@ -0,0 +1,81 @@ +package org.springblade.core.prometheus.data; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Getter; + +import java.util.List; +import java.util.Map; + +/** + * model details see https://www.consul.io/api/health.html#list-nodes-for-service + * + * @author consul + */ +@Getter +@Builder +public class ServiceHealth { + + @JsonProperty("Node") + private Node node; + + @JsonProperty("Service") + private Service service; + + @JsonProperty("Checks") + private List checks; + + @Getter + @Builder + public static class Node { + + @JsonProperty("Node") + private String name; + + @JsonProperty("Address") + private String address; + + @JsonProperty("Meta") + private Map meta; + } + + @Getter + @Builder + public static class Service { + + @JsonProperty("ID") + private String id; + + @JsonProperty("Service") + private String name; + + @JsonProperty("Tags") + private List tags; + + @JsonProperty("Address") + private String address; + + @JsonProperty("Meta") + private Map meta; + + @JsonProperty("Port") + private int port; + } + + @Getter + @Builder + public static class Check { + + @JsonProperty("Node") + private String node; + + @JsonProperty("CheckID") + private String checkId; + + @JsonProperty("Name") + private String name; + + @JsonProperty("Status") + private String status; + } +} diff --git a/blade-starter-prometheus/src/main/java/org/springblade/core/prometheus/endpoint/AgentEndpoint.java b/blade-starter-prometheus/src/main/java/org/springblade/core/prometheus/endpoint/AgentEndpoint.java new file mode 100644 index 0000000..4a75286 --- /dev/null +++ b/blade-starter-prometheus/src/main/java/org/springblade/core/prometheus/endpoint/AgentEndpoint.java @@ -0,0 +1,56 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.prometheus.endpoint; + +import com.alibaba.cloud.nacos.NacosDiscoveryProperties; +import lombok.RequiredArgsConstructor; +import org.springblade.core.auto.annotation.AutoIgnore; +import org.springblade.core.prometheus.data.Agent; +import org.springblade.core.prometheus.data.Config; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; + +/** + * consul agent api + * + * @author L.cm + */ +@AutoIgnore +@RestController +@RequiredArgsConstructor +public class AgentEndpoint { + private final NacosDiscoveryProperties properties; + + @GetMapping(value = "/v1/agent/self", produces = MediaType.APPLICATION_JSON_VALUE) + @ResponseBody + public Agent getNodes() { + Config config = Config.builder().dataCenter(properties.getGroup()).build(); + return Agent.builder().config(config).build(); + } + +} diff --git a/blade-starter-prometheus/src/main/java/org/springblade/core/prometheus/endpoint/PrometheusApi.java b/blade-starter-prometheus/src/main/java/org/springblade/core/prometheus/endpoint/PrometheusApi.java new file mode 100644 index 0000000..59e4252 --- /dev/null +++ b/blade-starter-prometheus/src/main/java/org/springblade/core/prometheus/endpoint/PrometheusApi.java @@ -0,0 +1,87 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.prometheus.endpoint; + +import lombok.RequiredArgsConstructor; +import org.springblade.core.auto.annotation.AutoIgnore; +import org.springblade.core.prometheus.pojo.AlertMessage; +import org.springblade.core.prometheus.pojo.TargetGroup; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.discovery.DiscoveryClient; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.http.ResponseEntity; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.*; + +import java.util.*; + +/** + * prometheus http sd + * + * @author L.cm + */ +@AutoIgnore +@RestController +@RequestMapping("actuator/prometheus") +@RequiredArgsConstructor +public class PrometheusApi { + private final String activeProfile; + private final DiscoveryClient discoveryClient; + private final ApplicationEventPublisher eventPublisher; + + @GetMapping("sd") + public List getList() { + List serviceIdList = discoveryClient.getServices(); + if (serviceIdList == null || serviceIdList.isEmpty()) { + return Collections.emptyList(); + } + List targetGroupList = new ArrayList<>(); + for (String serviceId : serviceIdList) { + List instanceList = discoveryClient.getInstances(serviceId); + List targets = new ArrayList<>(); + for (ServiceInstance instance : instanceList) { + targets.add(String.format("%s:%d", instance.getHost(), instance.getPort())); + } + Map labels = new HashMap<>(4); + // 1. 环境 + if (StringUtils.hasText(activeProfile)) { + labels.put("profile", activeProfile); + } + // 2. 服务名 + labels.put("__meta_prometheus_job", serviceId); + targetGroupList.add(new TargetGroup(targets, labels)); + } + return targetGroupList; + } + + @PostMapping("alerts") + public ResponseEntity postAlerts(@RequestBody AlertMessage message) { + eventPublisher.publishEvent(message); + return ResponseEntity.ok().build(); + } + +} diff --git a/blade-starter-prometheus/src/main/java/org/springblade/core/prometheus/endpoint/ReactivePrometheusApi.java b/blade-starter-prometheus/src/main/java/org/springblade/core/prometheus/endpoint/ReactivePrometheusApi.java new file mode 100644 index 0000000..ce0d5f7 --- /dev/null +++ b/blade-starter-prometheus/src/main/java/org/springblade/core/prometheus/endpoint/ReactivePrometheusApi.java @@ -0,0 +1,83 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.prometheus.endpoint; + +import lombok.RequiredArgsConstructor; +import org.springblade.core.auto.annotation.AutoIgnore; +import org.springblade.core.prometheus.pojo.AlertMessage; +import org.springblade.core.prometheus.pojo.TargetGroup; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.http.ResponseEntity; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.*; +import reactor.core.publisher.Flux; + +import java.util.HashMap; +import java.util.Map; + +/** + * prometheus http sd + * + * @author L.cm + */ +@AutoIgnore +@RestController +@RequestMapping("actuator/prometheus") +@RequiredArgsConstructor +public class ReactivePrometheusApi { + private final String activeProfile; + private final ReactiveDiscoveryClient discoveryClient; + private final ApplicationEventPublisher eventPublisher; + + @GetMapping("sd") + public Flux getList() { + return discoveryClient.getServices() + .flatMap(discoveryClient::getInstances) + .groupBy(ServiceInstance::getServiceId, instance -> + String.format("%s:%d", instance.getHost(), instance.getPort()) + ).flatMap(instanceGrouped -> { + Map labels = new HashMap<>(4); + // 1. 环境 + if (StringUtils.hasText(activeProfile)) { + labels.put("profile", activeProfile); + } + // 2. 服务名 + String serviceId = instanceGrouped.key(); + labels.put("__meta_prometheus_job", serviceId); + return instanceGrouped.collectList().map(targets -> new TargetGroup(targets, labels)); + }); + } + + @PostMapping("alerts") + public ResponseEntity postAlerts(@RequestBody AlertMessage message) { + eventPublisher.publishEvent(message); + return ResponseEntity.ok().build(); + } + +} diff --git a/blade-starter-prometheus/src/main/java/org/springblade/core/prometheus/endpoint/ServiceEndpoint.java b/blade-starter-prometheus/src/main/java/org/springblade/core/prometheus/endpoint/ServiceEndpoint.java new file mode 100644 index 0000000..61659d6 --- /dev/null +++ b/blade-starter-prometheus/src/main/java/org/springblade/core/prometheus/endpoint/ServiceEndpoint.java @@ -0,0 +1,144 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.prometheus.endpoint; + +import lombok.RequiredArgsConstructor; +import org.springblade.core.auto.annotation.AutoIgnore; +import org.springblade.core.prometheus.data.Service; +import org.springblade.core.prometheus.data.ServiceHealth; +import org.springblade.core.prometheus.service.RegistrationService; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.util.Assert; +import org.springframework.util.MultiValueMap; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Mono; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static java.util.stream.Collectors.toList; + +/** + * consul catalog api + * + * @author L.cm + */ +@AutoIgnore +@RestController +@RequiredArgsConstructor +public class ServiceEndpoint { + private static final String CONSUL_IDX_HEADER = "X-Consul-Index"; + private static final String QUERY_PARAM_WAIT = "wait"; + private static final String QUERY_PARAM_INDEX = "index"; + private static final Pattern WAIT_PATTERN = Pattern.compile("(\\d*)(m|s|ms|h)"); + private final RegistrationService registrationService; + + @GetMapping(value = "/v1/catalog/services", produces = MediaType.APPLICATION_JSON_VALUE) + public Mono>> getServiceNames( + @RequestParam(name = QUERY_PARAM_WAIT, required = false) String wait, + @RequestParam(name = QUERY_PARAM_INDEX, required = false) Long index) { + return registrationService.getServiceNames(getWaitMillis(wait), index) + .map(item -> createResponseEntity(item.getItem(), item.getChangeIndex())); + } + + @GetMapping(value = "/v1/catalog/service/{appName}", produces = MediaType.APPLICATION_JSON_VALUE) + public Mono>> getService(@PathVariable("appName") String appName, + @RequestParam(value = QUERY_PARAM_WAIT, required = false) String wait, + @RequestParam(value = QUERY_PARAM_INDEX, required = false) Long index) { + Objects.requireNonNull(appName, "service name can not be null"); + return registrationService.getService(appName, getWaitMillis(wait), index) + .map(item -> createResponseEntity(item.getItem(), item.getChangeIndex())); + } + + @GetMapping(value = "/v1/health/service/{appName}", produces = MediaType.APPLICATION_JSON_VALUE) + public Mono>> getServiceHealth(@PathVariable("appName") String appName, + @RequestParam(value = QUERY_PARAM_WAIT, required = false) String wait, + @RequestParam(value = QUERY_PARAM_INDEX, required = false) Long index) { + Assert.isTrue(appName != null, "service name can not be null"); + return registrationService.getService(appName, getWaitMillis(wait), index) + .map(item -> { + List services = item.getItem().stream() + .map(registrationService::getServiceHealth).collect(toList()); + return createResponseEntity(services, item.getChangeIndex()); + }); + } + + private static MultiValueMap createHeaders(long index) { + HttpHeaders headers = new HttpHeaders(); + headers.add(CONSUL_IDX_HEADER, String.valueOf(index)); + return headers; + } + + private static ResponseEntity createResponseEntity(T body, long index) { + return new ResponseEntity<>(body, createHeaders(index), HttpStatus.OK); + } + + /** + * Details to the wait behaviour can be found + * https://www.consul.io/api/index.html#blocking-queries + */ + private static long getWaitMillis(String wait) { + // default from consul docu + long millis = TimeUnit.MINUTES.toMillis(5); + if (wait != null) { + Matcher matcher = WAIT_PATTERN.matcher(wait); + if (matcher.matches()) { + long value = Long.parseLong(matcher.group(1)); + TimeUnit timeUnit = parseTimeUnit(matcher.group(2)); + millis = timeUnit.toMillis(value); + } else { + throw new IllegalArgumentException("Invalid wait pattern"); + } + } + return millis + ThreadLocalRandom.current().nextInt(((int) millis / 16) + 1); + } + + private static TimeUnit parseTimeUnit(String unit) { + switch (unit) { + case "h": + return TimeUnit.HOURS; + case "m": + return TimeUnit.MINUTES; + case "s": + return TimeUnit.SECONDS; + case "ms": + return TimeUnit.MILLISECONDS; + default: + throw new IllegalArgumentException("No valid time unit"); + } + } +} diff --git a/blade-starter-prometheus/src/main/java/org/springblade/core/prometheus/pojo/AlertInfo.java b/blade-starter-prometheus/src/main/java/org/springblade/core/prometheus/pojo/AlertInfo.java new file mode 100644 index 0000000..ccac514 --- /dev/null +++ b/blade-starter-prometheus/src/main/java/org/springblade/core/prometheus/pojo/AlertInfo.java @@ -0,0 +1,72 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.prometheus.pojo; + +import lombok.Data; + +import java.io.Serializable; +import java.time.OffsetDateTime; +import java.util.Map; + +/** + * 告警模型 + * + * @author L.cm + */ +@Data +public class AlertInfo implements Serializable { + + /** + * 状态 resolved|firing + */ + private String status; + /** + * 标签集合 + */ + private Map labels; + /** + * 注释集合 + */ + private Map annotations; + /** + * 开始时间 + */ + private OffsetDateTime startsAt; + /** + * 结束时间 + */ + private OffsetDateTime endsAt; + /** + * identifies the entity that caused the alert + */ + private String generatorURL; + /** + * fingerprint to identify the alert + */ + private String fingerprint; + +} diff --git a/blade-starter-prometheus/src/main/java/org/springblade/core/prometheus/pojo/AlertMessage.java b/blade-starter-prometheus/src/main/java/org/springblade/core/prometheus/pojo/AlertMessage.java new file mode 100644 index 0000000..d2b60d4 --- /dev/null +++ b/blade-starter-prometheus/src/main/java/org/springblade/core/prometheus/pojo/AlertMessage.java @@ -0,0 +1,84 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.prometheus.pojo; + +import lombok.Data; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; + +/** + * alert hook + * + * @author L.cm + */ +@Data +public class AlertMessage implements Serializable { + + /** + * 版本号 + */ + private String version; + /** + * 由于 “max_alerts” 而截断了多少警报 + */ + private Integer truncatedAlerts; + /** + * 分组 key + */ + private String groupKey; + /** + * 状态 resolved|firing + */ + private String status; + /** + * 接收者 + */ + private String receiver; + /** + * 分组 labels + */ + private Map groupLabels; + /** + * 通用 label + */ + private Map commonLabels; + /** + * 通用注解 + */ + private Map commonAnnotations; + /** + * 扩展 url 地址 + */ + private String externalURL; + /** + * alerts + */ + private List alerts; + +} diff --git a/blade-starter-prometheus/src/main/java/org/springblade/core/prometheus/pojo/TargetGroup.java b/blade-starter-prometheus/src/main/java/org/springblade/core/prometheus/pojo/TargetGroup.java new file mode 100644 index 0000000..0347662 --- /dev/null +++ b/blade-starter-prometheus/src/main/java/org/springblade/core/prometheus/pojo/TargetGroup.java @@ -0,0 +1,45 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.prometheus.pojo; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.List; +import java.util.Map; + +/** + * prometheus http sd 模型 + * + * @author L.cm + */ +@Getter +@RequiredArgsConstructor +public class TargetGroup { + private final List targets; + private final Map labels; +} diff --git a/blade-starter-prometheus/src/main/java/org/springblade/core/prometheus/service/RegistrationService.java b/blade-starter-prometheus/src/main/java/org/springblade/core/prometheus/service/RegistrationService.java new file mode 100644 index 0000000..aa4f4a2 --- /dev/null +++ b/blade-starter-prometheus/src/main/java/org/springblade/core/prometheus/service/RegistrationService.java @@ -0,0 +1,121 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ +package org.springblade.core.prometheus.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.prometheus.data.ChangeItem; +import org.springblade.core.prometheus.data.Service; +import org.springblade.core.prometheus.data.ServiceHealth; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.discovery.DiscoveryClient; +import reactor.core.publisher.Mono; + +import java.util.*; +import java.util.function.Supplier; + +/** + * Returns Services and List of Service with its last changed + * + * @author L.cm + */ +@Slf4j +@RequiredArgsConstructor +public class RegistrationService { + private static final String[] NO_SERVICE_TAGS = new String[0]; + private final DiscoveryClient discoveryClient; + + public Mono>> getServiceNames(long waitMillis, Long index) { + return returnDeferred(waitMillis, index, () -> { + List services = discoveryClient.getServices(); + Set set = new HashSet<>(services); + Map result = new HashMap<>(); + for (String item : set) { + result.put(item, NO_SERVICE_TAGS); + } + return result; + }); + } + + public Mono>> getService(String appName, long waitMillis, Long index) { + return returnDeferred(waitMillis, index, () -> { + List instances = discoveryClient.getInstances(appName); + List list = new ArrayList<>(); + if (instances == null || instances.isEmpty()) { + return Collections.emptyList(); + } + Set instSet = new HashSet<>(instances); + for (ServiceInstance instance : instSet) { + Service service = Service.builder() + .address(instance.getHost()) + .node(instance.getServiceId()) + .serviceAddress(instance.getHost()) + .servicePort(instance.getPort()) + .serviceName(instance.getServiceId()) + .serviceId(instance.getHost() + ":" + instance.getPort()) + .nodeMeta(Collections.emptyMap()) + .serviceMeta(instance.getMetadata()) + .serviceTags(Collections.emptyList()) + .build(); + list.add(service); + } + return list; + }); + } + + public ServiceHealth getServiceHealth(Service instanceInfo) { + String address = instanceInfo.getAddress(); + ServiceHealth.Node node = ServiceHealth.Node.builder() + .name(instanceInfo.getServiceName()) + .address(address) + .meta(Collections.emptyMap()) + .build(); + ServiceHealth.Service service = ServiceHealth.Service.builder() + .id(instanceInfo.getServiceId()) + .name(instanceInfo.getServiceName()) + .tags(Collections.emptyList()) + .address(address) + .meta(instanceInfo.getServiceMeta()) + .port(instanceInfo.getServicePort()) + .build(); + ServiceHealth.Check check = ServiceHealth.Check.builder() + .node(instanceInfo.getServiceName()) + .checkId("service:" + instanceInfo.getServiceId()) + .name("Service '" + instanceInfo.getServiceId() + "' check") + // nacos 实时性很高,可认定为健康 + .status("UP") + .build(); + return ServiceHealth.builder() + .node(node) + .service(service) + .checks(Collections.singletonList(check)) + .build(); + } + + private static Mono> returnDeferred(long waitMillis, Long index, Supplier fn) { + return Mono.just(new ChangeItem<>(fn.get(), System.currentTimeMillis())); + } +} diff --git a/blade-starter-prometheus/src/main/resources/blade-prometheus.yml b/blade-starter-prometheus/src/main/resources/blade-prometheus.yml new file mode 100644 index 0000000..01e80e4 --- /dev/null +++ b/blade-starter-prometheus/src/main/resources/blade-prometheus.yml @@ -0,0 +1,8 @@ +management: + endpoints: + web: + exposure: + include: "*" + metrics: + tags: + application: ${spring.application.name} diff --git a/blade-starter-redis/pom.xml b/blade-starter-redis/pom.xml new file mode 100644 index 0000000..0fc514b --- /dev/null +++ b/blade-starter-redis/pom.xml @@ -0,0 +1,55 @@ + + + 4.0.0 + + BladeX-Tool + org.springblade + ${revision} + + + blade-starter-redis + ${project.artifactId} + ${project.parent.version} + jar + + + + + org.springblade + blade-core-tool + + + org.springblade + blade-starter-jwt + + + + org.springframework.boot + spring-boot-starter-data-redis + + + org.redisson + redisson + + + + io.protostuff + protostuff-core + true + + + io.protostuff + protostuff-runtime + true + + + + org.springblade + blade-core-auto + provided + + + + diff --git a/blade-starter-redis/src/main/java/org/springblade/core/redis/cache/BladeRedis.java b/blade-starter-redis/src/main/java/org/springblade/core/redis/cache/BladeRedis.java new file mode 100644 index 0000000..ce5493c --- /dev/null +++ b/blade-starter-redis/src/main/java/org/springblade/core/redis/cache/BladeRedis.java @@ -0,0 +1,848 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.redis.cache; + +import lombok.Getter; +import org.springblade.core.tool.utils.CollectionUtil; +import org.springblade.core.tool.utils.NumberUtil; +import org.springframework.data.redis.core.*; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +import java.time.Duration; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +/** + * redis 工具 + * + * @author L.cm + */ +@Getter +@SuppressWarnings("unchecked") +public class BladeRedis { + private final RedisTemplate redisTemplate; + private final StringRedisTemplate stringRedisTemplate; + private final ValueOperations valueOps; + private final HashOperations hashOps; + private final ListOperations listOps; + private final SetOperations setOps; + private final ZSetOperations zSetOps; + + public BladeRedis(RedisTemplate redisTemplate, StringRedisTemplate stringRedisTemplate) { + this.redisTemplate = redisTemplate; + this.stringRedisTemplate = stringRedisTemplate; + Assert.notNull(redisTemplate, "redisTemplate is null"); + valueOps = redisTemplate.opsForValue(); + hashOps = redisTemplate.opsForHash(); + listOps = redisTemplate.opsForList(); + setOps = redisTemplate.opsForSet(); + zSetOps = redisTemplate.opsForZSet(); + } + + /** + * 设置缓存 + * + * @param cacheKey 缓存key + * @param value 缓存value + */ + public void set(CacheKey cacheKey, Object value) { + String key = cacheKey.getKey(); + Duration expire = cacheKey.getExpire(); + if (expire == null) { + set(key, value); + } else { + setEx(key, value, expire); + } + } + + /** + * 存放 key value 对到 redis。 + */ + public void set(String key, Object value) { + valueOps.set(key, value); + } + + /** + * 存放 key value 对到 redis,并将 key 的生存时间设为 seconds (以秒为单位)。 + * 如果 key 已经存在, SETEX 命令将覆写旧值。 + */ + public void setEx(String key, Object value, Duration timeout) { + valueOps.set(key, value, timeout); + } + + /** + * 存放 key value 对到 redis,并将 key 的生存时间设为 seconds (以秒为单位)。 + * 如果 key 已经存在, SETEX 命令将覆写旧值。 + */ + public void setEx(String key, Object value, Long seconds) { + valueOps.set(key, value, seconds, TimeUnit.SECONDS); + } + + /** + * 返回 key 所关联的 value 值 + * 如果 key 不存在那么返回特殊值 nil 。 + */ + @Nullable + public T get(String key) { + return (T) valueOps.get(key); + } + + /** + * 获取cache 为 null 时使用加载器,然后设置缓存 + * + * @param key cacheKey + * @param loader cache loader + * @param 泛型 + * @return 结果 + */ + @Nullable + public T get(String key, Supplier loader) { + T value = this.get(key); + if (value != null) { + return value; + } + value = loader.get(); + if (value == null) { + return null; + } + this.set(key, value); + return value; + } + + /** + * 返回 key 所关联的 value 值 + * 如果 key 不存在那么返回特殊值 nil 。 + */ + @Nullable + public T get(CacheKey cacheKey) { + return (T) valueOps.get(cacheKey.getKey()); + } + + /** + * 获取cache 为 null 时使用加载器,然后设置缓存 + * + * @param cacheKey cacheKey + * @param loader cache loader + * @param 泛型 + * @return 结果 + */ + @Nullable + public T get(CacheKey cacheKey, Supplier loader) { + String key = cacheKey.getKey(); + T value = this.get(key); + if (value != null) { + return value; + } + value = loader.get(); + if (value == null) { + return null; + } + this.set(cacheKey, value); + return value; + } + + /** + * 删除给定的一个 key + * 不存在的 key 会被忽略。 + */ + public Boolean del(String key) { + return redisTemplate.delete(key); + } + + /** + * 删除给定的一个 key + * 不存在的 key 会被忽略。 + */ + public Boolean del(CacheKey key) { + return redisTemplate.delete(key.getKey()); + } + + /** + * 删除给定的多个 key + * 不存在的 key 会被忽略。 + */ + public Long del(String... keys) { + return del(Arrays.asList(keys)); + } + + /** + * 删除给定的多个 key + * 不存在的 key 会被忽略。 + */ + public Long del(Collection keys) { + return redisTemplate.delete(keys); + } + + /** + * 查找所有符合给定模式 pattern 的 key 。 + * KEYS * 匹配数据库中所有 key 。 + * KEYS h?llo 匹配 hello , hallo 和 hxllo 等。 + * KEYS h*llo 匹配 hllo 和 heeeeello 等。 + * KEYS h[ae]llo 匹配 hello 和 hallo ,但不匹配 hillo 。 + * 特殊符号用 \ 隔开 + */ + public Set keys(String pattern) { + return redisTemplate.keys(pattern); + } + + /** + * 同时设置一个或多个 key-value 对。 + * 如果某个给定 key 已经存在,那么 MSET 会用新值覆盖原来的旧值,如果这不是你所希望的效果,请考虑使用 MSETNX 命令:它只会在所有给定 key 都不存在的情况下进行设置操作。 + * MSET 是一个原子性(atomic)操作,所有给定 key 都会在同一时间内被设置,某些给定 key 被更新而另一些给定 key 没有改变的情况,不可能发生。 + *

+	 * 例子:
+	 * Cache cache = RedisKit.use();			// 使用 Redis 的 cache
+	 * cache.mset("k1", "v1", "k2", "v2");		// 放入多个 key value 键值对
+	 * List list = cache.mget("k1", "k2");		// 利用多个键值得到上面代码放入的值
+	 * 
+ */ + public void mSet(Object... keysValues) { + valueOps.multiSet(CollectionUtil.toMap(keysValues)); + } + + /** + * 返回所有(一个或多个)给定 key 的值。 + * 如果给定的 key 里面,有某个 key 不存在,那么这个 key 返回特殊值 nil 。因此,该命令永不失败。 + */ + public List mGet(String... keys) { + return mGet(Arrays.asList(keys)); + } + + /** + * 返回所有(一个或多个)给定 key 的值。 + * 如果给定的 key 里面,有某个 key 不存在,那么这个 key 返回特殊值 nil 。因此,该命令永不失败。 + */ + public List mGet(Collection keys) { + return valueOps.multiGet(keys); + } + + /** + * 将 key 中储存的数字值减一。 + * 如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 DECR 操作。 + * 如果值包含错误的类型,或字符串类型的值不能表示为数字,那么返回一个错误。 + * 本操作的值限制在 64 位(bit)有符号数字表示之内。 + * 关于递增(increment) / 递减(decrement)操作的更多信息,请参见 INCR 命令。 + */ + public Long decr(String key) { + return stringRedisTemplate.opsForValue().decrement(key); + } + + /** + * 将 key 所储存的值减去减量 decrement 。 + * 如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 DECRBY 操作。 + * 如果值包含错误的类型,或字符串类型的值不能表示为数字,那么返回一个错误。 + * 本操作的值限制在 64 位(bit)有符号数字表示之内。 + * 关于更多递增(increment) / 递减(decrement)操作的更多信息,请参见 INCR 命令。 + */ + public Long decrBy(String key, long longValue) { + return stringRedisTemplate.opsForValue().decrement(key, longValue); + } + + /** + * 将 key 中储存的数字值增一。 + * 如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCR 操作。 + * 如果值包含错误的类型,或字符串类型的值不能表示为数字,那么返回一个错误。 + * 本操作的值限制在 64 位(bit)有符号数字表示之内。 + */ + public Long incr(String key) { + return stringRedisTemplate.opsForValue().increment(key); + } + + /** + * 将 key 所储存的值加上增量 increment 。 + * 如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCRBY 命令。 + * 如果值包含错误的类型,或字符串类型的值不能表示为数字,那么返回一个错误。 + * 本操作的值限制在 64 位(bit)有符号数字表示之内。 + * 关于递增(increment) / 递减(decrement)操作的更多信息,参见 INCR 命令。 + */ + public Long incrBy(String key, long longValue) { + return stringRedisTemplate.opsForValue().increment(key, longValue); + } + + /** + * 根据 key 获取递减的参数值 + */ + public Long getDecr(String key) { + return NumberUtil.toLong(stringRedisTemplate.opsForValue().get(key)); + } + + /** + * 根据 key 获取递增的参数值 + */ + public Long getIncr(String key) { + return NumberUtil.toLong(stringRedisTemplate.opsForValue().get(key)); + } + + /** + * 获取记数器的值 + */ + public Long getCounter(String key) { + return Long.valueOf(String.valueOf(valueOps.get(key))); + } + + /** + * 检查给定 key 是否存在。 + */ + public Boolean exists(String key) { + return redisTemplate.hasKey(key); + } + + /** + * 从当前数据库中随机返回(不删除)一个 key 。 + */ + public String randomKey() { + return redisTemplate.randomKey(); + } + + /** + * 将 key 改名为 newkey 。 + * 当 key 和 newkey 相同,或者 key 不存在时,返回一个错误。 + * 当 newkey 已经存在时, RENAME 命令将覆盖旧值。 + */ + public void rename(String oldkey, String newkey) { + redisTemplate.rename(oldkey, newkey); + } + + /** + * 将当前数据库的 key 移动到给定的数据库 db 当中。 + * 如果当前数据库(源数据库)和给定数据库(目标数据库)有相同名字的给定 key ,或者 key 不存在于当前数据库,那么 MOVE 没有任何效果。 + * 因此,也可以利用这一特性,将 MOVE 当作锁(locking)原语(primitive)。 + */ + public Boolean move(String key, int dbIndex) { + return redisTemplate.move(key, dbIndex); + } + + /** + * 为给定 key 设置生存时间,当 key 过期时(生存时间为 0 ),它会被自动删除。 + * 在 Redis 中,带有生存时间的 key 被称为『易失的』(volatile)。 + */ + public Boolean expire(String key, long seconds) { + return redisTemplate.expire(key, seconds, TimeUnit.SECONDS); + } + + /** + * 为给定 key 设置生存时间,当 key 过期时(生存时间为 0 ),它会被自动删除。 + * 在 Redis 中,带有生存时间的 key 被称为『易失的』(volatile)。 + */ + public Boolean expire(String key, Duration timeout) { + return expire(key, timeout.getSeconds()); + } + + /** + * EXPIREAT 的作用和 EXPIRE 类似,都用于为 key 设置生存时间。不同在于 EXPIREAT 命令接受的时间参数是 UNIX 时间戳(unix timestamp)。 + */ + public Boolean expireAt(String key, Date date) { + return redisTemplate.expireAt(key, date); + } + + /** + * EXPIREAT 的作用和 EXPIRE 类似,都用于为 key 设置生存时间。不同在于 EXPIREAT 命令接受的时间参数是 UNIX 时间戳(unix timestamp)。 + */ + public Boolean expireAt(String key, long unixTime) { + return expireAt(key, new Date(unixTime)); + } + + /** + * 这个命令和 EXPIRE 命令的作用类似,但是它以毫秒为单位设置 key 的生存时间,而不像 EXPIRE 命令那样,以秒为单位。 + */ + public Boolean pexpire(String key, long milliseconds) { + return redisTemplate.expire(key, milliseconds, TimeUnit.MILLISECONDS); + } + + /** + * 将给定 key 的值设为 value ,并返回 key 的旧值(old value)。 + * 当 key 存在但不是字符串类型时,返回一个错误。 + */ + public T getSet(String key, Object value) { + return (T) valueOps.getAndSet(key, value); + } + + /** + * 移除给定 key 的生存时间,将这个 key 从『易失的』(带生存时间 key )转换成『持久的』(一个不带生存时间、永不过期的 key )。 + */ + public Boolean persist(String key) { + return redisTemplate.persist(key); + } + + /** + * 返回 key 所储存的值的类型。 + */ + public String type(String key) { + return redisTemplate.type(key).code(); + } + + /** + * 以秒为单位,返回给定 key 的剩余生存时间(TTL, time to live)。 + */ + public Long ttl(String key) { + return redisTemplate.getExpire(key); + } + + /** + * 这个命令类似于 TTL 命令,但它以毫秒为单位返回 key 的剩余生存时间,而不是像 TTL 命令那样,以秒为单位。 + */ + public Long pttl(String key) { + return redisTemplate.getExpire(key, TimeUnit.MILLISECONDS); + } + + /** + * 将哈希表 key 中的域 field 的值设为 value 。 + * 如果 key 不存在,一个新的哈希表被创建并进行 HSET 操作。 + * 如果域 field 已经存在于哈希表中,旧值将被覆盖。 + */ + public void hSet(String key, Object field, Object value) { + hashOps.put(key, field, value); + } + + /** + * 同时将多个 field-value (域-值)对设置到哈希表 key 中。 + * 此命令会覆盖哈希表中已存在的域。 + * 如果 key 不存在,一个空哈希表被创建并执行 HMSET 操作。 + */ + public void hMset(String key, Map hash) { + hashOps.putAll(key, hash); + } + + /** + * 返回哈希表 key 中给定域 field 的值。 + */ + public T hGet(String key, Object field) { + return (T) hashOps.get(key, field); + } + + /** + * 返回哈希表 key 中,一个或多个给定域的值。 + * 如果给定的域不存在于哈希表,那么返回一个 nil 值。 + * 因为不存在的 key 被当作一个空哈希表来处理,所以对一个不存在的 key 进行 HMGET 操作将返回一个只带有 nil 值的表。 + */ + public List hmGet(String key, Object... fields) { + return hmGet(key, Arrays.asList(fields)); + } + + /** + * 返回哈希表 key 中,一个或多个给定域的值。 + * 如果给定的域不存在于哈希表,那么返回一个 nil 值。 + * 因为不存在的 key 被当作一个空哈希表来处理,所以对一个不存在的 key 进行 HMGET 操作将返回一个只带有 nil 值的表。 + */ + public List hmGet(String key, Collection hashKeys) { + return hashOps.multiGet(key, hashKeys); + } + + /** + * 删除哈希表 key 中的一个或多个指定域,不存在的域将被忽略。 + */ + public Long hDel(String key, Object... fields) { + return hashOps.delete(key, fields); + } + + /** + * 查看哈希表 key 中,给定域 field 是否存在。 + */ + public Boolean hExists(String key, Object field) { + return hashOps.hasKey(key, field); + } + + /** + * 返回哈希表 key 中,所有的域和值。 + * 在返回值里,紧跟每个域名(field name)之后是域的值(value),所以返回值的长度是哈希表大小的两倍。 + */ + public Map hGetAll(String key) { + return hashOps.entries(key); + } + + /** + * 返回哈希表 key 中所有域的值。 + */ + public List hVals(String key) { + return hashOps.values(key); + } + + /** + * 返回哈希表 key 中的所有域。 + * 底层实现此方法取名为 hfields 更为合适,在此仅为与底层保持一致 + */ + public Set hKeys(String key) { + return hashOps.keys(key); + } + + /** + * 返回哈希表 key 中域的数量。 + */ + public Long hLen(String key) { + return hashOps.size(key); + } + + /** + * 为哈希表 key 中的域 field 的值加上增量 increment 。 + * 增量也可以为负数,相当于对给定域进行减法操作。 + * 如果 key 不存在,一个新的哈希表被创建并执行 HINCRBY 命令。 + * 如果域 field 不存在,那么在执行命令前,域的值被初始化为 0 。 + * 对一个储存字符串值的域 field 执行 HINCRBY 命令将造成一个错误。 + * 本操作的值被限制在 64 位(bit)有符号数字表示之内。 + */ + public Long hIncrBy(String key, Object field, long value) { + return hashOps.increment(key, field, value); + } + + /** + * 为哈希表 key 中的域 field 加上浮点数增量 increment 。 + * 如果哈希表中没有域 field ,那么 HINCRBYFLOAT 会先将域 field 的值设为 0 ,然后再执行加法操作。 + * 如果键 key 不存在,那么 HINCRBYFLOAT 会先创建一个哈希表,再创建域 field ,最后再执行加法操作。 + * 当以下任意一个条件发生时,返回一个错误: + * 1:域 field 的值不是字符串类型(因为 redis 中的数字和浮点数都以字符串的形式保存,所以它们都属于字符串类型) + * 2:域 field 当前的值或给定的增量 increment 不能解释(parse)为双精度浮点数(double precision floating point number) + * HINCRBYFLOAT 命令的详细功能和 INCRBYFLOAT 命令类似,请查看 INCRBYFLOAT 命令获取更多相关信息。 + */ + public Double hIncrByFloat(String key, Object field, double value) { + return hashOps.increment(key, field, value); + } + + /** + * 返回列表 key 中,下标为 index 的元素。 + * 下标(index)参数 start 和 stop 都以 0 为底,也就是说,以 0 表示列表的第一个元素, + * 以 1 表示列表的第二个元素,以此类推。 + * 你也可以使用负数下标,以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,以此类推。 + * 如果 key 不是列表类型,返回一个错误。 + */ + public T lIndex(String key, long index) { + return (T) listOps.index(key, index); + } + + /** + * 返回列表 key 的长度。 + * 如果 key 不存在,则 key 被解释为一个空列表,返回 0 . + * 如果 key 不是列表类型,返回一个错误。 + */ + public Long lLen(String key) { + return listOps.size(key); + } + + /** + * 移除并返回列表 key 的头元素。 + */ + public T lPop(String key) { + return (T) listOps.leftPop(key); + } + + /** + * 将一个或多个值 value 插入到列表 key 的表头 + * 如果有多个 value 值,那么各个 value 值按从左到右的顺序依次插入到表头: 比如说, + * 对空列表 mylist 执行命令 LPUSH mylist a b c ,列表的值将是 c b a , + * 这等同于原子性地执行 LPUSH mylist a 、 LPUSH mylist b 和 LPUSH mylist c 三个命令。 + * 如果 key 不存在,一个空列表会被创建并执行 LPUSH 操作。 + * 当 key 存在但不是列表类型时,返回一个错误。 + */ + public Long lPush(String key, Object... values) { + return listOps.leftPush(key, values); + } + + /** + * 将列表 key 下标为 index 的元素的值设置为 value 。 + * 当 index 参数超出范围,或对一个空列表( key 不存在)进行 LSET 时,返回一个错误。 + * 关于列表下标的更多信息,请参考 LINDEX 命令。 + */ + public void lSet(String key, long index, Object value) { + listOps.set(key, index, value); + } + + /** + * 根据参数 count 的值,移除列表中与参数 value 相等的元素。 + * count 的值可以是以下几种: + * count > 0 : 从表头开始向表尾搜索,移除与 value 相等的元素,数量为 count 。 + * count < 0 : 从表尾开始向表头搜索,移除与 value 相等的元素,数量为 count 的绝对值。 + * count = 0 : 移除表中所有与 value 相等的值。 + */ + public Long lRem(String key, long count, Object value) { + return listOps.remove(key, count, value); + } + + /** + * 返回列表 key 中指定区间内的元素,区间以偏移量 start 和 stop 指定。 + * 下标(index)参数 start 和 stop 都以 0 为底,也就是说,以 0 表示列表的第一个元素,以 1 表示列表的第二个元素,以此类推。 + * 你也可以使用负数下标,以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,以此类推。 + *
+	 * 例子:
+	 * 获取 list 中所有数据:cache.lrange(listKey, 0, -1);
+	 * 获取 list 中下标 1 到 3 的数据: cache.lrange(listKey, 1, 3);
+	 * 
+ */ + public List lRange(String key, long start, long end) { + return listOps.range(key, start, end); + } + + /** + * 对一个列表进行修剪(trim),就是说,让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除。 + * 举个例子,执行命令 LTRIM list 0 2 ,表示只保留列表 list 的前三个元素,其余元素全部删除。 + * 下标(index)参数 start 和 stop 都以 0 为底,也就是说,以 0 表示列表的第一个元素,以 1 表示列表的第二个元素,以此类推。 + * 你也可以使用负数下标,以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,以此类推。 + * 当 key 不是列表类型时,返回一个错误。 + */ + public void lTrim(String key, long start, long end) { + listOps.trim(key, start, end); + } + + /** + * 移除并返回列表 key 的尾元素。 + */ + public T rPop(String key) { + return (T) listOps.rightPop(key); + } + + /** + * 将一个或多个值 value 插入到列表 key 的表尾(最右边)。 + * 如果有多个 value 值,那么各个 value 值按从左到右的顺序依次插入到表尾:比如 + * 对一个空列表 mylist 执行 RPUSH mylist a b c ,得出的结果列表为 a b c , + * 等同于执行命令 RPUSH mylist a 、 RPUSH mylist b 、 RPUSH mylist c 。 + * 如果 key 不存在,一个空列表会被创建并执行 RPUSH 操作。 + * 当 key 存在但不是列表类型时,返回一个错误。 + */ + public Long rPush(String key, Object... values) { + return listOps.rightPushAll(key, values); + } + + /** + * 命令 RPOPLPUSH 在一个原子时间内,执行以下两个动作: + * 将列表 source 中的最后一个元素(尾元素)弹出,并返回给客户端。 + * 将 source 弹出的元素插入到列表 destination ,作为 destination 列表的的头元素。 + */ + public T rPopLPush(String srcKey, String dstKey) { + return (T) listOps.rightPopAndLeftPush(srcKey, dstKey); + } + + /** + * 将一个或多个 member 元素加入到集合 key 当中,已经存在于集合的 member 元素将被忽略。 + * 假如 key 不存在,则创建一个只包含 member 元素作成员的集合。 + * 当 key 不是集合类型时,返回一个错误。 + */ + public Long sAdd(String key, Object... members) { + return setOps.add(key, members); + } + + /** + * 移除并返回集合中的一个随机元素。 + * 如果只想获取一个随机元素,但不想该元素从集合中被移除的话,可以使用 SRANDMEMBER 命令。 + */ + public T sPop(String key) { + return (T) setOps.pop(key); + } + + /** + * 返回集合 key 中的所有成员。 + * 不存在的 key 被视为空集合。 + */ + public Set sMembers(String key) { + return setOps.members(key); + } + + /** + * 判断 member 元素是否集合 key 的成员。 + */ + public boolean sIsMember(String key, Object member) { + return setOps.isMember(key, member); + } + + /** + * 返回多个集合的交集,多个集合由 keys 指定 + */ + public Set sInter(String key, String otherKey) { + return setOps.intersect(key, otherKey); + } + + /** + * 返回多个集合的交集,多个集合由 keys 指定 + */ + public Set sInter(String key, Collection otherKeys) { + return setOps.intersect(key, otherKeys); + } + + /** + * 返回集合中的一个随机元素。 + */ + public T sRandMember(String key) { + return (T) setOps.randomMember(key); + } + + /** + * 返回集合中的 count 个随机元素。 + * 从 Redis 2.6 版本开始, SRANDMEMBER 命令接受可选的 count 参数: + * 如果 count 为正数,且小于集合基数,那么命令返回一个包含 count 个元素的数组,数组中的元素各不相同。 + * 如果 count 大于等于集合基数,那么返回整个集合。 + * 如果 count 为负数,那么命令返回一个数组,数组中的元素可能会重复出现多次,而数组的长度为 count 的绝对值。 + * 该操作和 SPOP 相似,但 SPOP 将随机元素从集合中移除并返回,而 SRANDMEMBER 则仅仅返回随机元素,而不对集合进行任何改动。 + */ + public List sRandMember(String key, int count) { + return setOps.randomMembers(key, count); + } + + /** + * 移除集合 key 中的一个或多个 member 元素,不存在的 member 元素会被忽略。 + */ + public Long sRem(String key, Object... members) { + return setOps.remove(key, members); + } + + /** + * 返回多个集合的并集,多个集合由 keys 指定 + * 不存在的 key 被视为空集。 + */ + public Set sUnion(String key, String otherKey) { + return setOps.union(key, otherKey); + } + + /** + * 返回多个集合的并集,多个集合由 keys 指定 + * 不存在的 key 被视为空集。 + */ + public Set sUnion(String key, Collection otherKeys) { + return setOps.union(key, otherKeys); + } + + /** + * 返回一个集合的全部成员,该集合是所有给定集合之间的差集。 + * 不存在的 key 被视为空集。 + */ + public Set sDiff(String key, String otherKey) { + return setOps.difference(key, otherKey); + } + + /** + * 返回一个集合的全部成员,该集合是所有给定集合之间的差集。 + * 不存在的 key 被视为空集。 + */ + public Set sDiff(String key, Collection otherKeys) { + return setOps.difference(key, otherKeys); + } + + /** + * 将一个或多个 member 元素及其 score 值加入到有序集 key 当中。 + * 如果某个 member 已经是有序集的成员,那么更新这个 member 的 score 值, + * 并通过重新插入这个 member 元素,来保证该 member 在正确的位置上。 + */ + public Boolean zAdd(String key, Object member, double score) { + return zSetOps.add(key, member, score); + } + + /** + * 将一个或多个 member 元素及其 score 值加入到有序集 key 当中。 + * 如果某个 member 已经是有序集的成员,那么更新这个 member 的 score 值, + * 并通过重新插入这个 member 元素,来保证该 member 在正确的位置上。 + */ + public Long zAdd(String key, Map scoreMembers) { + Set> tuples = new HashSet<>(); + scoreMembers.forEach((k, v) -> { + tuples.add(new DefaultTypedTuple<>(k, v)); + }); + return zSetOps.add(key, tuples); + } + + /** + * 返回有序集 key 的基数。 + */ + public Long zCard(String key) { + return zSetOps.zCard(key); + } + + /** + * 返回有序集 key 中, score 值在 min 和 max 之间(默认包括 score 值等于 min 或 max )的成员的数量。 + * 关于参数 min 和 max 的详细使用方法,请参考 ZRANGEBYSCORE 命令。 + */ + public Long zCount(String key, double min, double max) { + return zSetOps.count(key, min, max); + } + + /** + * 为有序集 key 的成员 member 的 score 值加上增量 increment 。 + */ + public Double zIncrBy(String key, Object member, double score) { + return zSetOps.incrementScore(key, member, score); + } + + /** + * 返回有序集 key 中,指定区间内的成员。 + * 其中成员的位置按 score 值递增(从小到大)来排序。 + * 具有相同 score 值的成员按字典序(lexicographical order )来排列。 + * 如果你需要成员按 score 值递减(从大到小)来排列,请使用 ZREVRANGE 命令。 + */ + public Set zRange(String key, long start, long end) { + return zSetOps.range(key, start, end); + } + + /** + * 返回有序集 key 中,指定区间内的成员。 + * 其中成员的位置按 score 值递减(从大到小)来排列。 + * 具有相同 score 值的成员按字典序的逆序(reverse lexicographical order)排列。 + * 除了成员按 score 值递减的次序排列这一点外, ZREVRANGE 命令的其他方面和 ZRANGE 命令一样。 + */ + public Set zRevrange(String key, long start, long end) { + return zSetOps.reverseRange(key, start, end); + } + + /** + * 返回有序集 key 中,所有 score 值介于 min 和 max 之间(包括等于 min 或 max )的成员。 + * 有序集成员按 score 值递增(从小到大)次序排列。 + */ + public Set zRangeByScore(String key, double min, double max) { + return zSetOps.rangeByScore(key, min, max); + } + + /** + * 返回有序集 key 中成员 member 的排名。其中有序集成员按 score 值递增(从小到大)顺序排列。 + * 排名以 0 为底,也就是说, score 值最小的成员排名为 0 。 + * 使用 ZREVRANK 命令可以获得成员按 score 值递减(从大到小)排列的排名。 + */ + public Long zRank(String key, Object member) { + return zSetOps.rank(key, member); + } + + /** + * 返回有序集 key 中成员 member 的排名。其中有序集成员按 score 值递减(从大到小)排序。 + * 排名以 0 为底,也就是说, score 值最大的成员排名为 0 。 + * 使用 ZRANK 命令可以获得成员按 score 值递增(从小到大)排列的排名。 + */ + public Long zRevrank(String key, Object member) { + return zSetOps.reverseRank(key, member); + } + + /** + * 移除有序集 key 中的一个或多个成员,不存在的成员将被忽略。 + * 当 key 存在但不是有序集类型时,返回一个错误。 + */ + public Long zRem(String key, Object... members) { + return zSetOps.remove(key, members); + } + + /** + * 返回有序集 key 中,成员 member 的 score 值。 + * 如果 member 元素不是有序集 key 的成员,或 key 不存在,返回 nil 。 + */ + public Double zScore(String key, Object member) { + return zSetOps.score(key, member); + } + +} diff --git a/blade-starter-redis/src/main/java/org/springblade/core/redis/cache/CacheKey.java b/blade-starter-redis/src/main/java/org/springblade/core/redis/cache/CacheKey.java new file mode 100644 index 0000000..c57125a --- /dev/null +++ b/blade-starter-redis/src/main/java/org/springblade/core/redis/cache/CacheKey.java @@ -0,0 +1,59 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.redis.cache; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; +import org.springframework.lang.Nullable; + +import java.time.Duration; + +/** + * cache key 封装 + * + * @author L.cm + */ +@Getter +@ToString +@AllArgsConstructor +public class CacheKey { + /** + * redis key + */ + private final String key; + /** + * 超时时间 秒 + */ + @Nullable + private Duration expire; + + public CacheKey(String key) { + this.key = key; + } + +} diff --git a/blade-starter-redis/src/main/java/org/springblade/core/redis/cache/ICacheKey.java b/blade-starter-redis/src/main/java/org/springblade/core/redis/cache/ICacheKey.java new file mode 100644 index 0000000..b4cc420 --- /dev/null +++ b/blade-starter-redis/src/main/java/org/springblade/core/redis/cache/ICacheKey.java @@ -0,0 +1,80 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.redis.cache; + + +import org.springblade.core.tool.utils.ObjectUtil; +import org.springblade.core.tool.utils.StringPool; +import org.springblade.core.tool.utils.StringUtil; +import org.springframework.lang.Nullable; + +import java.time.Duration; + +/** + * cache key + * + * @author L.cm + */ +public interface ICacheKey { + + /** + * 获取前缀 + * + * @return key 前缀 + */ + String getPrefix(); + + /** + * 超时时间 + * + * @return 超时时间 + */ + @Nullable + default Duration getExpire() { + return null; + } + + /** + * 组装 cache key + * + * @param suffix 参数 + * @return cache key + */ + default CacheKey getKey(Object... suffix) { + String prefix = this.getPrefix(); + // 拼接参数 + String key; + if (ObjectUtil.isEmpty(suffix)) { + key = prefix; + } else { + key = prefix.concat(StringUtil.join(suffix, StringPool.COLON)); + } + Duration expire = this.getExpire(); + return expire == null ? new CacheKey(key) : new CacheKey(key, expire); + } + +} diff --git a/blade-starter-redis/src/main/java/org/springblade/core/redis/config/BladeRedisCacheAutoConfiguration.java b/blade-starter-redis/src/main/java/org/springblade/core/redis/config/BladeRedisCacheAutoConfiguration.java new file mode 100644 index 0000000..5d6620e --- /dev/null +++ b/blade-starter-redis/src/main/java/org/springblade/core/redis/config/BladeRedisCacheAutoConfiguration.java @@ -0,0 +1,123 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.redis.config; + +import org.springblade.core.jwt.config.JwtRedisConfiguration; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.cache.CacheManagerCustomizers; +import org.springframework.boot.autoconfigure.cache.CacheProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.cache.RedisCacheWriter; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.serializer.RedisSerializationContext; +import org.springframework.data.redis.serializer.RedisSerializer; +import org.springframework.lang.Nullable; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * 扩展redis-cache支持注解cacheName添加超时时间 + *

+ * + * @author L.cm + */ +@AutoConfiguration(before = JwtRedisConfiguration.class) +@EnableConfigurationProperties(CacheProperties.class) +public class BladeRedisCacheAutoConfiguration { + + /** + * 序列化方式 + */ + private final RedisSerializer redisSerializer; + private final CacheProperties cacheProperties; + private final CacheManagerCustomizers customizerInvoker; + @Nullable + private final RedisCacheConfiguration redisCacheConfiguration; + + BladeRedisCacheAutoConfiguration(RedisSerializer redisSerializer, + CacheProperties cacheProperties, + CacheManagerCustomizers customizerInvoker, + ObjectProvider redisCacheConfiguration) { + this.redisSerializer = redisSerializer; + this.cacheProperties = cacheProperties; + this.customizerInvoker = customizerInvoker; + this.redisCacheConfiguration = redisCacheConfiguration.getIfAvailable(); + } + + @Primary + @Bean("redisCacheManager") + public RedisCacheManager redisCacheManager(RedisConnectionFactory connectionFactory) { + RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory); + RedisCacheConfiguration cacheConfiguration = this.determineConfiguration(); + List cacheNames = this.cacheProperties.getCacheNames(); + Map initialCaches = new LinkedHashMap<>(); + if (!cacheNames.isEmpty()) { + Map cacheConfigMap = new LinkedHashMap<>(cacheNames.size()); + cacheNames.forEach(it -> cacheConfigMap.put(it, cacheConfiguration)); + initialCaches.putAll(cacheConfigMap); + } + boolean allowInFlightCacheCreation = true; + boolean enableTransactions = false; + RedisAutoCacheManager cacheManager = new RedisAutoCacheManager(redisCacheWriter, cacheConfiguration, initialCaches, allowInFlightCacheCreation); + cacheManager.setTransactionAware(enableTransactions); + return this.customizerInvoker.customize(cacheManager); + } + + private RedisCacheConfiguration determineConfiguration() { + if (this.redisCacheConfiguration != null) { + return this.redisCacheConfiguration; + } else { + CacheProperties.Redis redisProperties = this.cacheProperties.getRedis(); + RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig(); + config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer)); + if (redisProperties.getTimeToLive() != null) { + config = config.entryTtl(redisProperties.getTimeToLive()); + } + + if (redisProperties.getKeyPrefix() != null) { + config = config.prefixCacheNameWith(redisProperties.getKeyPrefix()); + } + + if (!redisProperties.isCacheNullValues()) { + config = config.disableCachingNullValues(); + } + + if (!redisProperties.isUseKeyPrefix()) { + config = config.disableKeyPrefix(); + } + + return config; + } + } +} diff --git a/blade-starter-redis/src/main/java/org/springblade/core/redis/config/BladeRedisProperties.java b/blade-starter-redis/src/main/java/org/springblade/core/redis/config/BladeRedisProperties.java new file mode 100644 index 0000000..e6ec517 --- /dev/null +++ b/blade-starter-redis/src/main/java/org/springblade/core/redis/config/BladeRedisProperties.java @@ -0,0 +1,95 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.redis.config; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.time.Duration; + +/** + * redis 配置 + * + * @author L.cm + */ +@Getter +@Setter +@ConfigurationProperties(BladeRedisProperties.PREFIX) +public class BladeRedisProperties { + public static final String PREFIX = "blade.redis"; + + /** + * 序列化方式 + */ + private SerializerType serializerType = SerializerType.ProtoStuff; + /** + * stream + */ + private Stream stream = new Stream(); + + public enum SerializerType { + /** + * 默认:ProtoStuff 序列化 + */ + ProtoStuff, + /** + * json 序列化 + */ + JSON, + /** + * jdk 序列化 + */ + JDK + } + + @Getter + @Setter + public static class Stream { + public static final String PREFIX = BladeRedisProperties.PREFIX + ".stream"; + /** + * 是否开启 stream + */ + boolean enable = false; + /** + * consumer group,默认:服务名 + 环境 + */ + String consumerGroup; + /** + * 消费者名称,默认:ip + 端口 + */ + String consumerName; + /** + * poll 批量大小 + */ + Integer pollBatchSize; + /** + * poll 超时时间 + */ + Duration pollTimeout; + } +} diff --git a/blade-starter-redis/src/main/java/org/springblade/core/redis/config/BladeRedisSerializerConfigAble.java b/blade-starter-redis/src/main/java/org/springblade/core/redis/config/BladeRedisSerializerConfigAble.java new file mode 100644 index 0000000..e214cfa --- /dev/null +++ b/blade-starter-redis/src/main/java/org/springblade/core/redis/config/BladeRedisSerializerConfigAble.java @@ -0,0 +1,75 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.redis.config; + +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer; +import org.springframework.data.redis.serializer.RedisSerializer; + +/** + * redis 序列化 + * + * @author L.cm + */ +public interface BladeRedisSerializerConfigAble { + + /** + * JSON序列化类型字段 + */ + String TYPE_NAME = "@class"; + + /** + * 序列化接口 + * + * @param properties 配置 + * @return RedisSerializer + */ + RedisSerializer redisSerializer(BladeRedisProperties properties); + + /** + * 默认的序列化方式 + * + * @param properties 配置 + * @return RedisSerializer + */ + default RedisSerializer defaultRedisSerializer(BladeRedisProperties properties) { + BladeRedisProperties.SerializerType serializerType = properties.getSerializerType(); + if (BladeRedisProperties.SerializerType.JDK == serializerType) { + /** + * SpringBoot扩展了ClassLoader,进行分离打包的时候,使用到JdkSerializationRedisSerializer的地方 + * 会因为ClassLoader的不同导致加载不到Class + * 指定使用项目的ClassLoader + * + * JdkSerializationRedisSerializer默认使用{@link sun.misc.Launcher.AppClassLoader} + * SpringBoot默认使用{@link org.springframework.boot.loader.LaunchedURLClassLoader} + */ + ClassLoader classLoader = this.getClass().getClassLoader(); + return new JdkSerializationRedisSerializer(classLoader); + } + return new GenericJackson2JsonRedisSerializer(TYPE_NAME); + } +} diff --git a/blade-starter-redis/src/main/java/org/springblade/core/redis/config/ProtoStuffSerializerConfiguration.java b/blade-starter-redis/src/main/java/org/springblade/core/redis/config/ProtoStuffSerializerConfiguration.java new file mode 100644 index 0000000..368375b --- /dev/null +++ b/blade-starter-redis/src/main/java/org/springblade/core/redis/config/ProtoStuffSerializerConfiguration.java @@ -0,0 +1,55 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.redis.config; + +import org.springblade.core.redis.serializer.ProtoStuffSerializer; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.data.redis.serializer.RedisSerializer; + +/** + * ProtoStuff 序列化配置 + * + * @author L.cm + */ +@AutoConfiguration(before = RedisTemplateConfiguration.class) +@ConditionalOnClass(name = "io.protostuff.Schema") +public class ProtoStuffSerializerConfiguration implements BladeRedisSerializerConfigAble { + + @Bean + @ConditionalOnMissingBean + @Override + public RedisSerializer redisSerializer(BladeRedisProperties properties) { + if (BladeRedisProperties.SerializerType.ProtoStuff == properties.getSerializerType()) { + return new ProtoStuffSerializer(); + } + return defaultRedisSerializer(properties); + } + +} diff --git a/blade-starter-redis/src/main/java/org/springblade/core/redis/config/RateLimiterAutoConfiguration.java b/blade-starter-redis/src/main/java/org/springblade/core/redis/config/RateLimiterAutoConfiguration.java new file mode 100644 index 0000000..6735379 --- /dev/null +++ b/blade-starter-redis/src/main/java/org/springblade/core/redis/config/RateLimiterAutoConfiguration.java @@ -0,0 +1,73 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.redis.config; + +import org.springblade.core.redis.ratelimiter.RedisRateLimiterAspect; +import org.springblade.core.redis.ratelimiter.RedisRateLimiterClient; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.core.env.Environment; +import org.springframework.core.io.ClassPathResource; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.script.DefaultRedisScript; +import org.springframework.data.redis.core.script.RedisScript; +import org.springframework.scripting.support.ResourceScriptSource; + +import java.util.List; + +/** + * 基于 redis 的分布式限流自动配置 + * + * @author L.cm + */ +@AutoConfiguration +@ConditionalOnProperty(value = "blade.redis.rate-limiter.enabled", havingValue = "true") +public class RateLimiterAutoConfiguration { + + private RedisScript redisRateLimiterScript() { + DefaultRedisScript redisScript = new DefaultRedisScript<>(); + redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("META-INF/scripts/blade_rate_limiter.lua"))); + redisScript.setResultType(Long.class); + return redisScript; + } + + @Bean + @ConditionalOnMissingBean + public RedisRateLimiterClient redisRateLimiter(StringRedisTemplate redisTemplate, + Environment environment) { + RedisScript redisRateLimiterScript = redisRateLimiterScript(); + return new RedisRateLimiterClient(redisTemplate, redisRateLimiterScript, environment); + } + + @Bean + @ConditionalOnMissingBean + public RedisRateLimiterAspect redisRateLimiterAspect(RedisRateLimiterClient rateLimiterClient) { + return new RedisRateLimiterAspect(rateLimiterClient); + } +} diff --git a/blade-starter-redis/src/main/java/org/springblade/core/redis/config/RedisAutoCacheManager.java b/blade-starter-redis/src/main/java/org/springblade/core/redis/config/RedisAutoCacheManager.java new file mode 100644 index 0000000..45dfd92 --- /dev/null +++ b/blade-starter-redis/src/main/java/org/springblade/core/redis/config/RedisAutoCacheManager.java @@ -0,0 +1,73 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.redis.config; + +import org.springblade.core.tool.utils.StringPool; +import org.springblade.core.tool.utils.StringUtil; +import org.springframework.boot.convert.DurationStyle; +import org.springframework.data.redis.cache.RedisCache; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.cache.RedisCacheWriter; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.Map; + +/** + * redis cache 扩展cache name自动化配置 + * + * @author L.cm + */ +public class RedisAutoCacheManager extends RedisCacheManager { + + public RedisAutoCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration, + Map initialCacheConfigurations, boolean allowInFlightCacheCreation) { + super(cacheWriter, defaultCacheConfiguration, initialCacheConfigurations, allowInFlightCacheCreation); + } + + @NonNull + @Override + protected RedisCache createRedisCache(@NonNull String name, @Nullable RedisCacheConfiguration cacheConfig) { + if (StringUtil.isBlank(name) || !name.contains(StringPool.HASH)) { + return super.createRedisCache(name, cacheConfig); + } + String[] cacheArray = name.split(StringPool.HASH); + if (cacheArray.length < 2) { + return super.createRedisCache(name, cacheConfig); + } + String cacheName = cacheArray[0]; + if (cacheConfig != null) { + Duration cacheAge = DurationStyle.detectAndParse(cacheArray[1], ChronoUnit.SECONDS);; + cacheConfig = cacheConfig.entryTtl(cacheAge); + } + return super.createRedisCache(cacheName, cacheConfig); + } + +} diff --git a/blade-starter-redis/src/main/java/org/springblade/core/redis/config/RedisCacheManagerConfig.java b/blade-starter-redis/src/main/java/org/springblade/core/redis/config/RedisCacheManagerConfig.java new file mode 100644 index 0000000..cf73389 --- /dev/null +++ b/blade-starter-redis/src/main/java/org/springblade/core/redis/config/RedisCacheManagerConfig.java @@ -0,0 +1,52 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.redis.config; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.cache.CacheManagerCustomizer; +import org.springframework.boot.autoconfigure.cache.CacheManagerCustomizers; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; + +import java.util.List; + +/** + * CacheManagerCustomizers配置 + * + * @author L.cm + */ +@AutoConfiguration +@ConditionalOnMissingBean(CacheManagerCustomizers.class) +public class RedisCacheManagerConfig { + + @Bean + public CacheManagerCustomizers cacheManagerCustomizers( + ObjectProvider>> customizers) { + return new CacheManagerCustomizers(customizers.getIfAvailable()); + } +} diff --git a/blade-starter-redis/src/main/java/org/springblade/core/redis/config/RedisStreamConfiguration.java b/blade-starter-redis/src/main/java/org/springblade/core/redis/config/RedisStreamConfiguration.java new file mode 100644 index 0000000..7a79bd3 --- /dev/null +++ b/blade-starter-redis/src/main/java/org/springblade/core/redis/config/RedisStreamConfiguration.java @@ -0,0 +1,140 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.redis.config; + +import org.springblade.core.launch.utils.INetUtil; +import org.springblade.core.redis.stream.DefaultRStreamTemplate; +import org.springblade.core.redis.stream.RStreamListenerDetector; +import org.springblade.core.redis.stream.RStreamTemplate; +import org.springblade.core.tool.utils.CharPool; +import org.springblade.core.tool.utils.StringUtil; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.core.env.Environment; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.stream.MapRecord; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.RedisSerializer; +import org.springframework.data.redis.stream.StreamMessageListenerContainer; +import org.springframework.data.redis.stream.StreamMessageListenerContainer.StreamMessageListenerContainerOptions; +import org.springframework.util.ErrorHandler; + +import java.time.Duration; + +/** + * redis Stream 配置 + * + * @author L.cm + */ +@AutoConfiguration +@ConditionalOnProperty( + prefix = BladeRedisProperties.Stream.PREFIX, + name = "enable", + havingValue = "true" +) +public class RedisStreamConfiguration { + + /** + * Spring 应用名 prop key + */ + private static final String SPRING_APP_NAME_KEY = "spring.application.name"; + /** + * The "active profiles" property name. + */ + private static final String ACTIVE_PROFILES_PROPERTY = "spring.profiles.active"; + + @Bean + @ConditionalOnMissingBean + public StreamMessageListenerContainerOptions> streamMessageListenerContainerOptions(BladeRedisProperties properties, + ObjectProvider errorHandlerObjectProvider) { + StreamMessageListenerContainer.StreamMessageListenerContainerOptionsBuilder> builder = StreamMessageListenerContainerOptions + .builder() + .keySerializer(RedisSerializer.string()) + .hashKeySerializer(RedisSerializer.string()) + .hashValueSerializer(RedisSerializer.byteArray()); + BladeRedisProperties.Stream streamProperties = properties.getStream(); + // 批量大小 + Integer pollBatchSize = streamProperties.getPollBatchSize(); + if (pollBatchSize != null && pollBatchSize > 0) { + builder.batchSize(pollBatchSize); + } + // poll 超时时间 + Duration pollTimeout = streamProperties.getPollTimeout(); + if (pollTimeout != null && !pollTimeout.isNegative()) { + builder.pollTimeout(pollTimeout); + } + // errorHandler + errorHandlerObjectProvider.ifAvailable((builder::errorHandler)); + // TODO L.cm executor + return builder.build(); + } + + @Bean + @ConditionalOnMissingBean + public StreamMessageListenerContainer> streamMessageListenerContainer(RedisConnectionFactory redisConnectionFactory, + StreamMessageListenerContainerOptions> streamMessageListenerContainerOptions) { + // 根据配置对象创建监听容器 + return StreamMessageListenerContainer.create(redisConnectionFactory, streamMessageListenerContainerOptions); + } + + @Bean + @ConditionalOnMissingBean + public RStreamListenerDetector streamListenerDetector(StreamMessageListenerContainer> streamMessageListenerContainer, + RedisTemplate redisTemplate, + ObjectProvider serverPropertiesObjectProvider, + BladeRedisProperties properties, + Environment environment) { + BladeRedisProperties.Stream streamProperties = properties.getStream(); + // 消费组名称 + String consumerGroup = streamProperties.getConsumerGroup(); + if (StringUtil.isBlank(consumerGroup)) { + String appName = environment.getRequiredProperty(SPRING_APP_NAME_KEY); + String profile = environment.getProperty(ACTIVE_PROFILES_PROPERTY); + consumerGroup = StringUtil.isBlank(profile) ? appName : appName + CharPool.COLON + profile; + } + // 消费者名称 + String consumerName = streamProperties.getConsumerName(); + if (StringUtil.isBlank(consumerName)) { + final StringBuilder consumerNameBuilder = new StringBuilder(INetUtil.getHostIp()); + serverPropertiesObjectProvider.ifAvailable(serverProperties -> { + consumerNameBuilder.append(CharPool.COLON).append(serverProperties.getPort()); + }); + consumerName = consumerNameBuilder.toString(); + } + return new RStreamListenerDetector(streamMessageListenerContainer, redisTemplate, consumerGroup, consumerName); + } + + @Bean + public RStreamTemplate streamTemplate(RedisTemplate redisTemplate) { + return new DefaultRStreamTemplate(redisTemplate); + } + +} diff --git a/blade-starter-redis/src/main/java/org/springblade/core/redis/config/RedisTemplateConfiguration.java b/blade-starter-redis/src/main/java/org/springblade/core/redis/config/RedisTemplateConfiguration.java new file mode 100644 index 0000000..bc7fd47 --- /dev/null +++ b/blade-starter-redis/src/main/java/org/springblade/core/redis/config/RedisTemplateConfiguration.java @@ -0,0 +1,92 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.redis.config; + +import org.springblade.core.jwt.config.JwtRedisConfiguration; +import org.springblade.core.redis.cache.BladeRedis; +import org.springblade.core.redis.serializer.RedisKeySerializer; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.data.redis.serializer.RedisSerializer; + +/** + * RedisTemplate 配置 + * + * @author L.cm + */ +@EnableCaching +@AutoConfiguration(before = {JwtRedisConfiguration.class, RedisAutoConfiguration.class}) +@EnableConfigurationProperties(BladeRedisProperties.class) +public class RedisTemplateConfiguration implements BladeRedisSerializerConfigAble { + + /** + * value 值 序列化 + * + * @return RedisSerializer + */ + @Bean + @ConditionalOnMissingBean(RedisSerializer.class) + @Override + public RedisSerializer redisSerializer(BladeRedisProperties properties) { + return defaultRedisSerializer(properties); + } + + @Bean(name = "redisTemplate") + @ConditionalOnMissingBean(name = "redisTemplate") + public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory, RedisSerializer redisSerializer) { + RedisTemplate redisTemplate = new RedisTemplate<>(); + // key 序列化 + RedisKeySerializer keySerializer = new RedisKeySerializer(); + redisTemplate.setKeySerializer(keySerializer); + redisTemplate.setHashKeySerializer(keySerializer); + // value 序列化 + redisTemplate.setValueSerializer(redisSerializer); + redisTemplate.setHashValueSerializer(redisSerializer); + redisTemplate.setConnectionFactory(redisConnectionFactory); + return redisTemplate; + } + + @Bean + @ConditionalOnMissingBean(ValueOperations.class) + public ValueOperations valueOperations(RedisTemplate redisTemplate) { + return redisTemplate.opsForValue(); + } + + @Bean + public BladeRedis bladeRedis(RedisTemplate redisTemplate, StringRedisTemplate stringRedisTemplate) { + return new BladeRedis(redisTemplate, stringRedisTemplate); + } + +} diff --git a/blade-starter-redis/src/main/java/org/springblade/core/redis/lock/BladeLockAutoConfiguration.java b/blade-starter-redis/src/main/java/org/springblade/core/redis/lock/BladeLockAutoConfiguration.java new file mode 100644 index 0000000..4269f08 --- /dev/null +++ b/blade-starter-redis/src/main/java/org/springblade/core/redis/lock/BladeLockAutoConfiguration.java @@ -0,0 +1,161 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.redis.lock; + +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.*; +import org.springblade.core.tool.utils.StringUtil; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; + +/** + * 分布式锁自动化配置 + * + * @author L.cm + */ +@AutoConfiguration +@ConditionalOnClass(RedissonClient.class) +@EnableConfigurationProperties(BladeLockProperties.class) +@ConditionalOnProperty(value = "blade.lock.enabled", havingValue = "true") +public class BladeLockAutoConfiguration { + + private static Config singleConfig(BladeLockProperties properties) { + Config config = new Config(); + SingleServerConfig serversConfig = config.useSingleServer(); + serversConfig.setAddress(properties.getAddress()); + String password = properties.getPassword(); + if (StringUtil.isNotBlank(password)) { + serversConfig.setPassword(password); + } + serversConfig.setDatabase(properties.getDatabase()); + serversConfig.setConnectionPoolSize(properties.getPoolSize()); + serversConfig.setConnectionMinimumIdleSize(properties.getIdleSize()); + serversConfig.setIdleConnectionTimeout(properties.getConnectionTimeout()); + serversConfig.setConnectTimeout(properties.getConnectionTimeout()); + serversConfig.setTimeout(properties.getTimeout()); + return config; + } + + private static Config masterSlaveConfig(BladeLockProperties properties) { + Config config = new Config(); + MasterSlaveServersConfig serversConfig = config.useMasterSlaveServers(); + serversConfig.setMasterAddress(properties.getMasterAddress()); + serversConfig.addSlaveAddress(properties.getSlaveAddress()); + String password = properties.getPassword(); + if (StringUtil.isNotBlank(password)) { + serversConfig.setPassword(password); + } + serversConfig.setDatabase(properties.getDatabase()); + serversConfig.setMasterConnectionPoolSize(properties.getPoolSize()); + serversConfig.setMasterConnectionMinimumIdleSize(properties.getIdleSize()); + serversConfig.setSlaveConnectionPoolSize(properties.getPoolSize()); + serversConfig.setSlaveConnectionMinimumIdleSize(properties.getIdleSize()); + serversConfig.setIdleConnectionTimeout(properties.getConnectionTimeout()); + serversConfig.setConnectTimeout(properties.getConnectionTimeout()); + serversConfig.setTimeout(properties.getTimeout()); + return config; + } + + private static Config sentinelConfig(BladeLockProperties properties) { + Config config = new Config(); + SentinelServersConfig serversConfig = config.useSentinelServers(); + serversConfig.setMasterName(properties.getMasterName()); + serversConfig.addSentinelAddress(properties.getSentinelAddress()); + String password = properties.getPassword(); + if (StringUtil.isNotBlank(password)) { + serversConfig.setPassword(password); + } + serversConfig.setDatabase(properties.getDatabase()); + serversConfig.setMasterConnectionPoolSize(properties.getPoolSize()); + serversConfig.setMasterConnectionMinimumIdleSize(properties.getIdleSize()); + serversConfig.setSlaveConnectionPoolSize(properties.getPoolSize()); + serversConfig.setSlaveConnectionMinimumIdleSize(properties.getIdleSize()); + serversConfig.setIdleConnectionTimeout(properties.getConnectionTimeout()); + serversConfig.setConnectTimeout(properties.getConnectionTimeout()); + serversConfig.setTimeout(properties.getTimeout()); + return config; + } + + private static Config clusterConfig(BladeLockProperties properties) { + Config config = new Config(); + ClusterServersConfig serversConfig = config.useClusterServers(); + serversConfig.addNodeAddress(properties.getNodeAddress()); + String password = properties.getPassword(); + if (StringUtil.isNotBlank(password)) { + serversConfig.setPassword(password); + } + serversConfig.setMasterConnectionPoolSize(properties.getPoolSize()); + serversConfig.setMasterConnectionMinimumIdleSize(properties.getIdleSize()); + serversConfig.setSlaveConnectionPoolSize(properties.getPoolSize()); + serversConfig.setSlaveConnectionMinimumIdleSize(properties.getIdleSize()); + serversConfig.setIdleConnectionTimeout(properties.getConnectionTimeout()); + serversConfig.setConnectTimeout(properties.getConnectionTimeout()); + serversConfig.setTimeout(properties.getTimeout()); + return config; + } + + @Bean + @ConditionalOnMissingBean + public RedisLockClient redisLockClient(BladeLockProperties properties) { + return new RedisLockClientImpl(redissonClient(properties)); + } + + @Bean + @ConditionalOnMissingBean + public RedisLockAspect redisLockAspect(RedisLockClient redisLockClient) { + return new RedisLockAspect(redisLockClient); + } + + private static RedissonClient redissonClient(BladeLockProperties properties) { + BladeLockProperties.Mode mode = properties.getMode(); + Config config; + switch (mode) { + case sentinel: + config = sentinelConfig(properties); + break; + case cluster: + config = clusterConfig(properties); + break; + case master: + config = masterSlaveConfig(properties); + break; + case single: + config = singleConfig(properties); + break; + default: + config = new Config(); + break; + } + return Redisson.create(config); + } + +} diff --git a/blade-starter-redis/src/main/java/org/springblade/core/redis/lock/BladeLockProperties.java b/blade-starter-redis/src/main/java/org/springblade/core/redis/lock/BladeLockProperties.java new file mode 100644 index 0000000..208810f --- /dev/null +++ b/blade-starter-redis/src/main/java/org/springblade/core/redis/lock/BladeLockProperties.java @@ -0,0 +1,114 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.redis.lock; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * 分布式锁配置 + * + * @author L.cm + */ +@Getter +@Setter +@ConfigurationProperties(BladeLockProperties.PREFIX) +public class BladeLockProperties { + public static final String PREFIX = "blade.lock"; + + /** + * 是否开启:默认为:false,便于生成配置提示。 + */ + private Boolean enabled = Boolean.FALSE; + /** + * 单机配置:redis 服务地址 + */ + private String address = "redis://127.0.0.1:6379"; + /** + * 密码配置 + */ + private String password; + /** + * db + */ + private Integer database = 0; + /** + * 连接池大小 + */ + private Integer poolSize = 20; + /** + * 最小空闲连接数 + */ + private Integer idleSize = 5; + /** + * 连接空闲超时,单位:毫秒 + */ + private Integer idleTimeout = 60000; + /** + * 连接超时,单位:毫秒 + */ + private Integer connectionTimeout = 3000; + /** + * 命令等待超时,单位:毫秒 + */ + private Integer timeout = 10000; + /** + * 集群模式,单机:single,主从:master,哨兵模式:sentinel,集群模式:cluster + */ + private Mode mode = Mode.single; + /** + * 主从模式,主地址 + */ + private String masterAddress; + /** + * 主从模式,从地址 + */ + private String[] slaveAddress; + /** + * 哨兵模式:主名称 + */ + private String masterName; + /** + * 哨兵模式地址 + */ + private String[] sentinelAddress; + /** + * 集群模式节点地址 + */ + private String[] nodeAddress; + + public enum Mode { + /** + * 集群模式,单机:single,主从:master,哨兵模式:sentinel,集群模式:cluster + */ + single, + master, + sentinel, + cluster + } +} diff --git a/blade-starter-redis/src/main/java/org/springblade/core/redis/lock/LockType.java b/blade-starter-redis/src/main/java/org/springblade/core/redis/lock/LockType.java new file mode 100644 index 0000000..0e20090 --- /dev/null +++ b/blade-starter-redis/src/main/java/org/springblade/core/redis/lock/LockType.java @@ -0,0 +1,43 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.redis.lock; + +/** + * 锁类型 + * + * @author lcm + */ +public enum LockType { + /** + * 重入锁 + */ + REENTRANT, + /** + * 公平锁 + */ + FAIR +} diff --git a/blade-starter-redis/src/main/java/org/springblade/core/redis/lock/RedisLock.java b/blade-starter-redis/src/main/java/org/springblade/core/redis/lock/RedisLock.java new file mode 100644 index 0000000..8c10cf6 --- /dev/null +++ b/blade-starter-redis/src/main/java/org/springblade/core/redis/lock/RedisLock.java @@ -0,0 +1,91 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.redis.lock; + +import java.lang.annotation.*; +import java.util.concurrent.TimeUnit; + +/** + * 分布式锁注解,redisson,支持的锁的种类有很多,适合注解形式的只有重入锁、公平锁 + * + *

+ * 1. 可重入锁(Reentrant Lock) + * 2. 公平锁(Fair Lock) + * 3. 联锁(MultiLock) + * 4. 红锁(RedLock) + * 5. 读写锁(ReadWriteLock) + *

+ * + * @author L.cm + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@Documented +public @interface RedisLock { + + /** + * 分布式锁的 key,必须:请保持唯一性 + * + * @return key + */ + String value(); + + /** + * 分布式锁参数,可选,支持 spring el # 读取方法参数和 @ 读取 spring bean + * + * @return param + */ + String param() default ""; + + /** + * 等待锁超时时间,默认30 + * + * @return int + */ + long waitTime() default 30; + + /** + * 自动解锁时间,自动解锁时间一定得大于方法执行时间,否则会导致锁提前释放,默认100 + * + * @return int + */ + long leaseTime() default 100; + + /** + * 时间单位,默认为秒 + * + * @return 时间单位 + */ + TimeUnit timeUnit() default TimeUnit.SECONDS; + + /** + * 默认公平锁 + * + * @return LockType + */ + LockType type() default LockType.FAIR; +} diff --git a/blade-starter-redis/src/main/java/org/springblade/core/redis/lock/RedisLockAspect.java b/blade-starter-redis/src/main/java/org/springblade/core/redis/lock/RedisLockAspect.java new file mode 100644 index 0000000..3965453 --- /dev/null +++ b/blade-starter-redis/src/main/java/org/springblade/core/redis/lock/RedisLockAspect.java @@ -0,0 +1,112 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.redis.lock; + +import lombok.RequiredArgsConstructor; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.reflect.MethodSignature; +import org.springblade.core.tool.spel.BladeExpressionEvaluator; +import org.springblade.core.tool.utils.CharPool; +import org.springblade.core.tool.utils.StringUtil; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.expression.AnnotatedElementKey; +import org.springframework.expression.EvaluationContext; +import org.springframework.util.Assert; + +import java.lang.reflect.Method; +import java.util.concurrent.TimeUnit; + +/** + * redis 分布式锁 + * + * @author L.cm + */ +@Aspect +@RequiredArgsConstructor +public class RedisLockAspect implements ApplicationContextAware { + + /** + * 表达式处理 + */ + private static final BladeExpressionEvaluator EVALUATOR = new BladeExpressionEvaluator(); + /** + * redis 限流服务 + */ + private final RedisLockClient redisLockClient; + private ApplicationContext applicationContext; + + /** + * AOP 环切 注解 @RedisLock + */ + @Around("@annotation(redisLock)") + public Object aroundRedisLock(ProceedingJoinPoint point, RedisLock redisLock) { + String lockName = redisLock.value(); + Assert.hasText(lockName, "@RedisLock value must have length; it must not be null or empty"); + // el 表达式 + String lockParam = redisLock.param(); + // 表达式不为空 + String lockKey; + if (StringUtil.isNotBlank(lockParam)) { + String evalAsText = evalLockParam(point, lockParam); + lockKey = lockName + CharPool.COLON + evalAsText; + } else { + lockKey = lockName; + } + LockType lockType = redisLock.type(); + long waitTime = redisLock.waitTime(); + long leaseTime = redisLock.leaseTime(); + TimeUnit timeUnit = redisLock.timeUnit(); + return redisLockClient.lock(lockKey, lockType, waitTime, leaseTime, timeUnit, point::proceed); + } + + /** + * 计算参数表达式 + * + * @param point ProceedingJoinPoint + * @param lockParam lockParam + * @return 结果 + */ + private String evalLockParam(ProceedingJoinPoint point, String lockParam) { + MethodSignature ms = (MethodSignature) point.getSignature(); + Method method = ms.getMethod(); + Object[] args = point.getArgs(); + Object target = point.getTarget(); + Class targetClass = target.getClass(); + EvaluationContext context = EVALUATOR.createContext(method, args, target, targetClass, applicationContext); + AnnotatedElementKey elementKey = new AnnotatedElementKey(method, targetClass); + return EVALUATOR.evalAsText(lockParam, elementKey, context); + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } +} diff --git a/blade-starter-redis/src/main/java/org/springblade/core/redis/lock/RedisLockClient.java b/blade-starter-redis/src/main/java/org/springblade/core/redis/lock/RedisLockClient.java new file mode 100644 index 0000000..de8295d --- /dev/null +++ b/blade-starter-redis/src/main/java/org/springblade/core/redis/lock/RedisLockClient.java @@ -0,0 +1,100 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.redis.lock; + +import org.springblade.core.tool.function.CheckedSupplier; + +import java.util.concurrent.TimeUnit; + +/** + * 锁客户端 + * + * @author L.cm + */ +public interface RedisLockClient { + + /** + * 尝试获取锁 + * + * @param lockName 锁名 + * @param lockType 锁类型 + * @param waitTime 等待时间 + * @param leaseTime 自动解锁时间,自动解锁时间一定得大于方法执行时间 + * @param timeUnit 时间参数 + * @return 是否成功 + * @throws InterruptedException InterruptedException + */ + boolean tryLock(String lockName, LockType lockType, long waitTime, long leaseTime, TimeUnit timeUnit) throws InterruptedException; + + /** + * 解锁 + * + * @param lockName 锁名 + * @param lockType 锁类型 + */ + void unLock(String lockName, LockType lockType); + + /** + * 自定获取锁后执行方法 + * + * @param lockName 锁名 + * @param lockType 锁类型 + * @param waitTime 等待锁超时时间 + * @param leaseTime 自动解锁时间,自动解锁时间一定得大于方法执行时间,否则会导致锁提前释放,默认100 + * @param timeUnit 时间单位 + * @param supplier 获取锁后的回调 + * @return 返回的数据 + */ + T lock(String lockName, LockType lockType, long waitTime, long leaseTime, TimeUnit timeUnit, CheckedSupplier supplier); + + /** + * 公平锁 + * + * @param lockName 锁名 + * @param waitTime 等待锁超时时间 + * @param leaseTime 自动解锁时间,自动解锁时间一定得大于方法执行时间,否则会导致锁提前释放,默认100 + * @param supplier 获取锁后的回调 + * @return 返回的数据 + */ + default T lockFair(String lockName, long waitTime, long leaseTime, CheckedSupplier supplier) { + return lock(lockName, LockType.FAIR, waitTime, leaseTime, TimeUnit.SECONDS, supplier); + } + + /** + * 可重入锁 + * + * @param lockName 锁名 + * @param waitTime 等待锁超时时间 + * @param leaseTime 自动解锁时间,自动解锁时间一定得大于方法执行时间,否则会导致锁提前释放,默认100 + * @param supplier 获取锁后的回调 + * @return 返回的数据 + */ + default T lockReentrant(String lockName, long waitTime, long leaseTime, CheckedSupplier supplier) { + return lock(lockName, LockType.REENTRANT, waitTime, leaseTime, TimeUnit.SECONDS, supplier); + } + +} diff --git a/blade-starter-redis/src/main/java/org/springblade/core/redis/lock/RedisLockClientImpl.java b/blade-starter-redis/src/main/java/org/springblade/core/redis/lock/RedisLockClientImpl.java new file mode 100644 index 0000000..85d1260 --- /dev/null +++ b/blade-starter-redis/src/main/java/org/springblade/core/redis/lock/RedisLockClientImpl.java @@ -0,0 +1,88 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.redis.lock; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springblade.core.tool.function.CheckedSupplier; +import org.springblade.core.tool.utils.Exceptions; + +import java.util.concurrent.TimeUnit; + +/** + * 锁客户端 + * + * @author L.cm + */ +@Slf4j +@RequiredArgsConstructor +public class RedisLockClientImpl implements RedisLockClient { + private final RedissonClient redissonClient; + + @Override + public boolean tryLock(String lockName, LockType lockType, long waitTime, long leaseTime, TimeUnit timeUnit) throws InterruptedException { + RLock lock = getLock(lockName, lockType); + return lock.tryLock(waitTime, leaseTime, timeUnit); + } + + @Override + public void unLock(String lockName, LockType lockType) { + RLock lock = getLock(lockName, lockType); + // 仅仅在已经锁定和当前线程持有锁时解锁 + if (lock.isLocked() && lock.isHeldByCurrentThread()) { + lock.unlock(); + } + } + + private RLock getLock(String lockName, LockType lockType) { + RLock lock; + if (LockType.REENTRANT == lockType) { + lock = redissonClient.getLock(lockName); + } else { + lock = redissonClient.getFairLock(lockName); + } + return lock; + } + + @Override + public T lock(String lockName, LockType lockType, long waitTime, long leaseTime, TimeUnit timeUnit, CheckedSupplier supplier) { + try { + boolean result = this.tryLock(lockName, lockType, waitTime, leaseTime, timeUnit); + if (result) { + return supplier.get(); + } + } catch (Throwable e) { + throw Exceptions.unchecked(e); + } finally { + this.unLock(lockName, lockType); + } + return null; + } + +} diff --git a/blade-starter-redis/src/main/java/org/springblade/core/redis/ratelimiter/RateLimiter.java b/blade-starter-redis/src/main/java/org/springblade/core/redis/ratelimiter/RateLimiter.java new file mode 100644 index 0000000..27fca13 --- /dev/null +++ b/blade-starter-redis/src/main/java/org/springblade/core/redis/ratelimiter/RateLimiter.java @@ -0,0 +1,76 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.redis.ratelimiter; + +import java.lang.annotation.*; +import java.util.concurrent.TimeUnit; + +/** + * 分布式 限流注解,默认速率为 600/ms + * + * @author L.cm + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@Documented +public @interface RateLimiter { + + /** + * 限流的 key 支持,必须:请保持唯一性 + * + * @return key + */ + String value(); + + /** + * 限流的参数,可选,支持 spring el # 读取方法参数和 @ 读取 spring bean + * + * @return param + */ + String param() default ""; + + /** + * 支持的最大请求,默认: 100 + * + * @return 请求数 + */ + long max() default 100L; + + /** + * 持续时间,默认: 3600 + * + * @return 持续时间 + */ + long ttl() default 1L; + + /** + * 时间单位,默认为分 + * + * @return TimeUnit + */ + TimeUnit timeUnit() default TimeUnit.MINUTES; +} diff --git a/blade-starter-redis/src/main/java/org/springblade/core/redis/ratelimiter/RateLimiterClient.java b/blade-starter-redis/src/main/java/org/springblade/core/redis/ratelimiter/RateLimiterClient.java new file mode 100644 index 0000000..26a9b02 --- /dev/null +++ b/blade-starter-redis/src/main/java/org/springblade/core/redis/ratelimiter/RateLimiterClient.java @@ -0,0 +1,100 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.redis.ratelimiter; + + +import org.springblade.core.tool.function.CheckedSupplier; +import org.springblade.core.tool.utils.Exceptions; + +import java.util.concurrent.TimeUnit; + +/** + * RateLimiter 限流 Client + * + * @author L.cm + */ +public interface RateLimiterClient { + + /** + * 服务是否被限流 + * + * @param key 自定义的key,请保证唯一 + * @param max 支持的最大请求 + * @param ttl 时间,单位默认为秒(seconds) + * @return 是否允许 + */ + default boolean isAllowed(String key, long max, long ttl) { + return this.isAllowed(key, max, ttl, TimeUnit.SECONDS); + } + + /** + * 服务是否被限流 + * + * @param key 自定义的key,请保证唯一 + * @param max 支持的最大请求 + * @param ttl 时间 + * @param timeUnit 时间单位 + * @return 是否允许 + */ + boolean isAllowed(String key, long max, long ttl, TimeUnit timeUnit); + + /** + * 服务限流,被限制时抛出 RateLimiterException 异常,需要自行处理异常 + * + * @param key 自定义的key,请保证唯一 + * @param max 支持的最大请求 + * @param ttl 时间 + * @param supplier Supplier 函数式 + * @return 函数执行结果 + */ + default T allow(String key, long max, long ttl, CheckedSupplier supplier) { + return allow(key, max, ttl, TimeUnit.SECONDS, supplier); + } + + /** + * 服务限流,被限制时抛出 RateLimiterException 异常,需要自行处理异常 + * + * @param key 自定义的key,请保证唯一 + * @param max 支持的最大请求 + * @param ttl 时间 + * @param timeUnit 时间单位 + * @param supplier Supplier 函数式 + * @param + * @return 函数执行结果 + */ + default T allow(String key, long max, long ttl, TimeUnit timeUnit, CheckedSupplier supplier) { + boolean isAllowed = this.isAllowed(key, max, ttl, timeUnit); + if (isAllowed) { + try { + return supplier.get(); + } catch (Throwable e) { + throw Exceptions.unchecked(e); + } + } + throw new RateLimiterException(key, max, ttl, timeUnit); + } +} diff --git a/blade-starter-redis/src/main/java/org/springblade/core/redis/ratelimiter/RateLimiterException.java b/blade-starter-redis/src/main/java/org/springblade/core/redis/ratelimiter/RateLimiterException.java new file mode 100644 index 0000000..2195ff2 --- /dev/null +++ b/blade-starter-redis/src/main/java/org/springblade/core/redis/ratelimiter/RateLimiterException.java @@ -0,0 +1,52 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.redis.ratelimiter; + +import lombok.Getter; + +import java.util.concurrent.TimeUnit; + +/** + * 限流异常 + * + * @author L.cm + */ +@Getter +public class RateLimiterException extends RuntimeException { + private final String key; + private final long max; + private final long ttl; + private final TimeUnit timeUnit; + + public RateLimiterException(String key, long max, long ttl, TimeUnit timeUnit) { + super(String.format("您的访问次数已超限:%s,速率:%d/%ds", key, max, timeUnit.toSeconds(ttl))); + this.key = key; + this.max = max; + this.ttl = ttl; + this.timeUnit = timeUnit; + } +} diff --git a/blade-starter-redis/src/main/java/org/springblade/core/redis/ratelimiter/RedisRateLimiterAspect.java b/blade-starter-redis/src/main/java/org/springblade/core/redis/ratelimiter/RedisRateLimiterAspect.java new file mode 100644 index 0000000..4afedfb --- /dev/null +++ b/blade-starter-redis/src/main/java/org/springblade/core/redis/ratelimiter/RedisRateLimiterAspect.java @@ -0,0 +1,111 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.redis.ratelimiter; + +import lombok.RequiredArgsConstructor; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.reflect.MethodSignature; +import org.springblade.core.tool.spel.BladeExpressionEvaluator; +import org.springblade.core.tool.utils.CharPool; +import org.springblade.core.tool.utils.StringUtil; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.expression.AnnotatedElementKey; +import org.springframework.expression.EvaluationContext; +import org.springframework.lang.NonNull; +import org.springframework.util.Assert; + +import java.lang.reflect.Method; +import java.util.concurrent.TimeUnit; + +/** + * redis 限流 + * + * @author L.cm + */ +@Aspect +@RequiredArgsConstructor +public class RedisRateLimiterAspect implements ApplicationContextAware { + /** + * 表达式处理 + */ + private final BladeExpressionEvaluator evaluator = new BladeExpressionEvaluator(); + /** + * redis 限流服务 + */ + private final RedisRateLimiterClient rateLimiterClient; + private ApplicationContext applicationContext; + + /** + * AOP 环切 注解 @RateLimiter + */ + @Around("@annotation(limiter)") + public Object aroundRateLimiter(ProceedingJoinPoint point, RateLimiter limiter) throws Throwable { + String limitKey = limiter.value(); + Assert.hasText(limitKey, "@RateLimiter value must have length; it must not be null or empty"); + // el 表达式 + String limitParam = limiter.param(); + // 表达式不为空 + String rateKey; + if (StringUtil.isNotBlank(limitParam)) { + String evalAsText = evalLimitParam(point, limitParam); + rateKey = limitKey + CharPool.COLON + evalAsText; + } else { + rateKey = limitKey; + } + long max = limiter.max(); + long ttl = limiter.ttl(); + TimeUnit timeUnit = limiter.timeUnit(); + return rateLimiterClient.allow(rateKey, max, ttl, timeUnit, point::proceed); + } + + /** + * 计算参数表达式 + * + * @param point ProceedingJoinPoint + * @param limitParam limitParam + * @return 结果 + */ + private String evalLimitParam(ProceedingJoinPoint point, String limitParam) { + MethodSignature ms = (MethodSignature) point.getSignature(); + Method method = ms.getMethod(); + Object[] args = point.getArgs(); + Object target = point.getTarget(); + Class targetClass = target.getClass(); + EvaluationContext context = evaluator.createContext(method, args, target, targetClass, applicationContext); + AnnotatedElementKey elementKey = new AnnotatedElementKey(method, targetClass); + return evaluator.evalAsText(limitParam, elementKey, context); + } + + @Override + public void setApplicationContext(@NonNull ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } +} diff --git a/blade-starter-redis/src/main/java/org/springblade/core/redis/ratelimiter/RedisRateLimiterClient.java b/blade-starter-redis/src/main/java/org/springblade/core/redis/ratelimiter/RedisRateLimiterClient.java new file mode 100644 index 0000000..90e8b9c --- /dev/null +++ b/blade-starter-redis/src/main/java/org/springblade/core/redis/ratelimiter/RedisRateLimiterClient.java @@ -0,0 +1,84 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.redis.ratelimiter; + +import lombok.RequiredArgsConstructor; +import org.springblade.core.tool.utils.CharPool; +import org.springframework.core.env.Environment; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.script.RedisScript; + +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * redis 限流服务 + * + * @author dream.lu + */ +@RequiredArgsConstructor +public class RedisRateLimiterClient implements RateLimiterClient { + /** + * redis 限流 key 前缀 + */ + private static final String REDIS_KEY_PREFIX = "limiter:"; + /** + * 失败的默认返回值 + */ + private static final long FAIL_CODE = 0; + /** + * redisTemplate + */ + private final StringRedisTemplate redisTemplate; + /** + * redisScript + */ + private final RedisScript script; + /** + * env + */ + private final Environment environment; + + @Override + public boolean isAllowed(String key, long max, long ttl, TimeUnit timeUnit) { + // redis key + String redisKeyBuilder = REDIS_KEY_PREFIX + + getApplicationName(environment) + CharPool.COLON + key; + List keys = Collections.singletonList(redisKeyBuilder); + // 转为毫秒,pexpire + long ttlMillis = timeUnit.toMillis(ttl); + // 执行命令 + Long result = this.redisTemplate.execute(this.script, keys, max + "", ttlMillis + ""); + return result != null && result != FAIL_CODE; + } + + private static String getApplicationName(Environment environment) { + return environment.getProperty("spring.application.name", ""); + } + +} diff --git a/blade-starter-redis/src/main/java/org/springblade/core/redis/serializer/BytesWrapper.java b/blade-starter-redis/src/main/java/org/springblade/core/redis/serializer/BytesWrapper.java new file mode 100644 index 0000000..8531584 --- /dev/null +++ b/blade-starter-redis/src/main/java/org/springblade/core/redis/serializer/BytesWrapper.java @@ -0,0 +1,61 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.redis.serializer; + +/** + * redis序列化辅助类.单纯的泛型无法定义通用schema,原因是无法通过泛型T得到Class + * + * @author L.cm + */ +public class BytesWrapper implements Cloneable { + private T value; + + public BytesWrapper() { + } + + public BytesWrapper(T value) { + this.value = value; + } + + public void setValue(T value) { + this.value = value; + } + + public T getValue() { + return value; + } + + @Override + @SuppressWarnings("unchecked") + public BytesWrapper clone() { + try { + return (BytesWrapper) super.clone(); + } catch (CloneNotSupportedException e) { + return new BytesWrapper<>(); + } + } +} diff --git a/blade-starter-redis/src/main/java/org/springblade/core/redis/serializer/ProtoStuffSerializer.java b/blade-starter-redis/src/main/java/org/springblade/core/redis/serializer/ProtoStuffSerializer.java new file mode 100644 index 0000000..4001d4c --- /dev/null +++ b/blade-starter-redis/src/main/java/org/springblade/core/redis/serializer/ProtoStuffSerializer.java @@ -0,0 +1,71 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.redis.serializer; + +import io.protostuff.LinkedBuffer; +import io.protostuff.ProtobufIOUtil; +import io.protostuff.Schema; +import io.protostuff.runtime.RuntimeSchema; +import org.springblade.core.tool.utils.ObjectUtil; +import org.springframework.data.redis.serializer.RedisSerializer; +import org.springframework.data.redis.serializer.SerializationException; + +/** + * ProtoStuff 序列化 + * + * @author L.cm + */ +public class ProtoStuffSerializer implements RedisSerializer { + private final Schema schema; + + public ProtoStuffSerializer() { + this.schema = RuntimeSchema.getSchema(BytesWrapper.class); + } + + @Override + public byte[] serialize(Object object) throws SerializationException { + if (object == null) { + return null; + } + LinkedBuffer buffer = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE); + try { + return ProtobufIOUtil.toByteArray(new BytesWrapper<>(object), schema, buffer); + } finally { + buffer.clear(); + } + } + + @Override + public Object deserialize(byte[] bytes) throws SerializationException { + if (ObjectUtil.isEmpty(bytes)) { + return null; + } + BytesWrapper wrapper = new BytesWrapper<>(); + ProtobufIOUtil.mergeFrom(bytes, wrapper, schema); + return wrapper.getValue(); + } +} diff --git a/blade-starter-redis/src/main/java/org/springblade/core/redis/serializer/RedisKeySerializer.java b/blade-starter-redis/src/main/java/org/springblade/core/redis/serializer/RedisKeySerializer.java new file mode 100644 index 0000000..2c19a76 --- /dev/null +++ b/blade-starter-redis/src/main/java/org/springblade/core/redis/serializer/RedisKeySerializer.java @@ -0,0 +1,84 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.redis.serializer; + +import org.springframework.cache.interceptor.SimpleKey; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.data.redis.serializer.RedisSerializer; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Objects; + +/** + * 将redis key序列化为字符串 + * + *

+ * spring cache中的简单基本类型直接使用 StringRedisSerializer 会有问题 + *

+ * + * @author L.cm + */ +public class RedisKeySerializer implements RedisSerializer { + private final Charset charset; + private final ConversionService converter; + + public RedisKeySerializer() { + this(StandardCharsets.UTF_8); + } + + public RedisKeySerializer(Charset charset) { + Objects.requireNonNull(charset, "Charset must not be null"); + this.charset = charset; + this.converter = DefaultConversionService.getSharedInstance(); + } + + @Override + public Object deserialize(byte[] bytes) { + // redis keys 会用到反序列化 + if (bytes == null) { + return null; + } + return new String(bytes, charset); + } + + @Override + public byte[] serialize(Object object) { + Objects.requireNonNull(object, "redis key is null"); + String key; + if (object instanceof SimpleKey) { + key = ""; + } else if (object instanceof String) { + key = (String) object; + } else { + key = converter.convert(object, String.class); + } + return key.getBytes(this.charset); + } + +} diff --git a/blade-starter-redis/src/main/java/org/springblade/core/redis/stream/DefaultRStreamTemplate.java b/blade-starter-redis/src/main/java/org/springblade/core/redis/stream/DefaultRStreamTemplate.java new file mode 100644 index 0000000..5540235 --- /dev/null +++ b/blade-starter-redis/src/main/java/org/springblade/core/redis/stream/DefaultRStreamTemplate.java @@ -0,0 +1,122 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.redis.stream; + +import org.springframework.data.redis.connection.RedisStreamCommands; +import org.springframework.data.redis.connection.stream.MapRecord; +import org.springframework.data.redis.connection.stream.Record; +import org.springframework.data.redis.connection.stream.RecordId; +import org.springframework.data.redis.core.RedisCallback; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.StreamOperations; +import org.springframework.data.redis.core.convert.RedisCustomConversions; +import org.springframework.data.redis.serializer.RedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +/** + * 默认的 RStreamTemplate + * + * @author L.cm + */ +public class DefaultRStreamTemplate implements RStreamTemplate { + private static final RedisCustomConversions CUSTOM_CONVERSIONS = new RedisCustomConversions(); + private final RedisTemplate redisTemplate; + private final StreamOperations streamOperations; + + public DefaultRStreamTemplate(RedisTemplate redisTemplate) { + this.redisTemplate = redisTemplate; + this.streamOperations = redisTemplate.opsForStream(); + } + + @Override + public RecordId send(Record record) { + // 1. MapRecord + if (record instanceof MapRecord) { + return streamOperations.add(record); + } + String stream = Objects.requireNonNull(record.getStream(), "RStreamTemplate send stream name is null."); + Object recordValue = Objects.requireNonNull(record.getValue(), "RStreamTemplate send stream: " + stream + " value is null."); + Class valueClass = recordValue.getClass(); + // 2. 普通类型的 ObjectRecord + if (CUSTOM_CONVERSIONS.isSimpleType(valueClass)) { + return streamOperations.add(record); + } + // 3. 自定义类型处理 + Map payload = new HashMap<>(); + payload.put(RStreamTemplate.OBJECT_PAYLOAD_KEY, recordValue); + MapRecord mapRecord = MapRecord.create(stream, payload); + return streamOperations.add(mapRecord); + } + + @Override + public RecordId send(String name, String key, byte[] data, RedisStreamCommands.XAddOptions options) { + RedisSerializer stringSerializer = StringRedisSerializer.UTF_8; + byte[] nameBytes = Objects.requireNonNull(stringSerializer.serialize(name), "redis stream name is null."); + byte[] keyBytes = Objects.requireNonNull(stringSerializer.serialize(key), "redis stream key is null."); + Map mapDate = Collections.singletonMap(keyBytes, data); + return redisTemplate.execute((RedisCallback) redis -> { + RedisStreamCommands streamCommands = redis.streamCommands(); + return streamCommands.xAdd(MapRecord.create(nameBytes, mapDate), options); + }); + } + + @Override + public Long delete(String name, String... recordIds) { + return streamOperations.delete(name, recordIds); + } + + @Override + public Long delete(String name, RecordId... recordIds) { + return streamOperations.delete(name, recordIds); + } + + @Override + public Long trim(String name, long count, boolean approximateTrimming) { + return streamOperations.trim(name, count, approximateTrimming); + } + + @Override + public Long acknowledge(String name, String group, String... recordIds) { + return streamOperations.acknowledge(name, group, recordIds); + } + + @Override + public Long acknowledge(String name, String group, RecordId... recordIds) { + return streamOperations.acknowledge(name, group, recordIds); + } + + @Override + public Long acknowledge(String group, Record record) { + return streamOperations.acknowledge(group, record); + } + +} diff --git a/blade-starter-redis/src/main/java/org/springblade/core/redis/stream/MessageModel.java b/blade-starter-redis/src/main/java/org/springblade/core/redis/stream/MessageModel.java new file mode 100644 index 0000000..635c3ac --- /dev/null +++ b/blade-starter-redis/src/main/java/org/springblade/core/redis/stream/MessageModel.java @@ -0,0 +1,46 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.redis.stream; + +/** + * 消息类型 + * + * @author L.cm + */ +public enum MessageModel { + + /** + * 广播 + */ + BROADCASTING, + + /** + * 集群消息 + */ + CLUSTERING; + +} diff --git a/blade-starter-redis/src/main/java/org/springblade/core/redis/stream/RStreamListener.java b/blade-starter-redis/src/main/java/org/springblade/core/redis/stream/RStreamListener.java new file mode 100644 index 0000000..681addd --- /dev/null +++ b/blade-starter-redis/src/main/java/org/springblade/core/redis/stream/RStreamListener.java @@ -0,0 +1,90 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.redis.stream; + +import java.lang.annotation.*; + +/** + * 基于 redis 的 stream 监听 + * + * @author L.cm + */ +@Documented +@Inherited +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface RStreamListener { + + /** + * Queue name + * + * @return String + */ + String name(); + + /** + * consumer group,默认为服务名 + 环境 + * + * @return String + */ + String group() default ""; + + /** + * 消息方式,集群模式和广播模式,如果想让所有订阅者收到所有消息,广播是一个不错的选择。 + * + * @return MessageModel + */ + MessageModel messageModel() default MessageModel.CLUSTERING; + + /** + * offsetModel,默认:LAST_CONSUMED + * + *

+ * 0-0 : 从开始的地方读。 + * $ :表示从尾部开始消费,只接受新消息,当前 Stream 消息会全部忽略。 + * > : 读取所有新到达的元素,这些元素的id大于消费组使用的最后一个元素。 + *

+ * + * @return ReadOffsetModel + */ + ReadOffsetModel offsetModel() default ReadOffsetModel.LAST_CONSUMED; + + /** + * 自动 ack + * + * @return boolean + */ + boolean autoAcknowledge() default false; + + /** + * 读取原始的 bytes 数据 + * + * @return boolean + */ + boolean readRawBytes() default false; + +} diff --git a/blade-starter-redis/src/main/java/org/springblade/core/redis/stream/RStreamListenerDetector.java b/blade-starter-redis/src/main/java/org/springblade/core/redis/stream/RStreamListenerDetector.java new file mode 100644 index 0000000..5ba34b1 --- /dev/null +++ b/blade-starter-redis/src/main/java/org/springblade/core/redis/stream/RStreamListenerDetector.java @@ -0,0 +1,169 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.redis.stream; + +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.tool.utils.ReflectUtil; +import org.springblade.core.tool.utils.StringUtil; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.data.redis.RedisSystemException; +import org.springframework.data.redis.connection.stream.*; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.StreamOperations; +import org.springframework.data.redis.stream.StreamMessageListenerContainer; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.Map; + +/** + * Redisson 监听器 + * + * @author L.cm + */ +@Slf4j +public class RStreamListenerDetector implements BeanPostProcessor, InitializingBean { + private final StreamMessageListenerContainer> streamMessageListenerContainer; + private final RedisTemplate redisTemplate; + private final String consumerGroup; + private final String consumerName; + + public RStreamListenerDetector(StreamMessageListenerContainer> streamMessageListenerContainer, + RedisTemplate redisTemplate, String consumerGroup, String consumerName) { + this.streamMessageListenerContainer = streamMessageListenerContainer; + this.redisTemplate = redisTemplate; + this.consumerGroup = consumerGroup; + this.consumerName = consumerName; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + Class userClass = ClassUtils.getUserClass(bean); + ReflectionUtils.doWithMethods(userClass, method -> { + RStreamListener listener = AnnotationUtils.findAnnotation(method, RStreamListener.class); + if (listener != null) { + String streamKey = listener.name(); + Assert.hasText(streamKey, "@RStreamListener name must not be empty."); + log.info("Found @RStreamListener on bean:{} method:{}", beanName, method); + // 校验 method,method 入参数大于等于1 + int paramCount = method.getParameterCount(); + if (paramCount > 1) { + throw new IllegalArgumentException("@RStreamListener on method " + method + " parameter count must less or equal to 1."); + } + // streamOffset + ReadOffset readOffset = listener.offsetModel().getReadOffset(); + StreamOffset streamOffset = StreamOffset.create(streamKey, readOffset); + // 消费模式 + MessageModel messageModel = listener.messageModel(); + if (MessageModel.BROADCASTING == messageModel) { + broadCast(streamOffset, bean, method, listener.readRawBytes()); + } else { + String groupId = StringUtil.isNotBlank(listener.group()) ? listener.group() : consumerGroup; + Consumer consumer = Consumer.from(groupId, consumerName); + // 如果需要,创建 group + createGroupIfNeed(redisTemplate, streamKey, readOffset, groupId); + cluster(consumer, streamOffset, listener, bean, method); + } + } + }, ReflectionUtils.USER_DECLARED_METHODS); + return bean; + } + + private void broadCast(StreamOffset streamOffset, Object bean, Method method, boolean isReadRawBytes) { + streamMessageListenerContainer.receive(streamOffset, (message) -> { + // MapBackedRecord + invokeMethod(bean, method, message, isReadRawBytes); + }); + } + + private void cluster(Consumer consumer, StreamOffset streamOffset, RStreamListener listener, Object bean, Method method) { + boolean autoAcknowledge = listener.autoAcknowledge(); + StreamMessageListenerContainer.ConsumerStreamReadRequest readRequest = StreamMessageListenerContainer.StreamReadRequest.builder(streamOffset).consumer(consumer).autoAcknowledge(autoAcknowledge).build(); + StreamOperations opsForStream = redisTemplate.opsForStream(); + streamMessageListenerContainer.register(readRequest, (message) -> { + // MapBackedRecord + invokeMethod(bean, method, message, listener.readRawBytes()); + // ack + if (!autoAcknowledge) { + opsForStream.acknowledge(consumer.getGroup(), message); + } + }); + } + + private static void createGroupIfNeed(RedisTemplate redisTemplate, String streamKey, ReadOffset readOffset, String group) { + StreamOperations opsForStream = redisTemplate.opsForStream(); + try { + StreamInfo.XInfoGroups groups = opsForStream.groups(streamKey); + if (groups.stream().noneMatch((x) -> group.equals(x.groupName()))) { + opsForStream.createGroup(streamKey, readOffset, group); + } + } catch (RedisSystemException e) { + // RedisCommandExecutionException: ERR no such key + opsForStream.createGroup(streamKey, group); + } + } + + private void invokeMethod(Object bean, Method method, MapRecord mapRecord, boolean isReadRawBytes) { + // 支持没有参数的方法 + if (method.getParameterCount() == 0) { + ReflectUtil.invokeMethod(method, bean); + return; + } + if (isReadRawBytes) { + ReflectUtil.invokeMethod(method, bean, mapRecord); + } else { + ReflectUtil.invokeMethod(method, bean, getRecordValue(mapRecord)); + } + } + + private Object getRecordValue(MapRecord mapRecord) { + Map messageValue = mapRecord.getValue(); + if (messageValue.containsKey(RStreamTemplate.OBJECT_PAYLOAD_KEY)) { + byte[] payloads = messageValue.get(RStreamTemplate.OBJECT_PAYLOAD_KEY); + Object deserialize = redisTemplate.getValueSerializer().deserialize(payloads); + return ObjectRecord.create(mapRecord.getStream(), deserialize).withId(mapRecord.getId()); + } else { + return mapRecord.mapEntries(entry -> { + String key = entry.getKey(); + Object value = redisTemplate.getValueSerializer().deserialize(entry.getValue()); + return Collections.singletonMap(key, value).entrySet().iterator().next(); + }); + } + } + + @Override + public void afterPropertiesSet() throws Exception { + streamMessageListenerContainer.start(); + } + +} diff --git a/blade-starter-redis/src/main/java/org/springblade/core/redis/stream/RStreamTemplate.java b/blade-starter-redis/src/main/java/org/springblade/core/redis/stream/RStreamTemplate.java new file mode 100644 index 0000000..525a431 --- /dev/null +++ b/blade-starter-redis/src/main/java/org/springblade/core/redis/stream/RStreamTemplate.java @@ -0,0 +1,270 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.redis.stream; + +import org.springframework.data.redis.connection.RedisStreamCommands; +import org.springframework.data.redis.connection.stream.MapRecord; +import org.springframework.data.redis.connection.stream.ObjectRecord; +import org.springframework.data.redis.connection.stream.Record; +import org.springframework.data.redis.connection.stream.RecordId; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.lang.Nullable; + +import java.util.Collections; +import java.util.Map; +import java.util.function.Function; + +/** + * 基于 redis Stream 的消息发布器 + * + * @author L.cm + */ +public interface RStreamTemplate { + + /** + * 自定义 pojo 类型 key + */ + String OBJECT_PAYLOAD_KEY = "@payload"; + + /** + * 方便多 redis 数据源使用 + * + * @param redisTemplate RedisTemplate + * @return MicaRedisCache + */ + static RStreamTemplate use(RedisTemplate redisTemplate) { + return new DefaultRStreamTemplate(redisTemplate); + } + + /** + * 发布消息 + * + * @param name 队列名 + * @param value 消息 + * @return 消息id + */ + default RecordId send(String name, Object value) { + return send(ObjectRecord.create(name, value)); + } + + /** + * 发布消息 + * + * @param name 队列名 + * @param key 消息key + * @param value 消息 + * @return 消息id + */ + default RecordId send(String name, String key, Object value) { + return send(name, Collections.singletonMap(key, value)); + } + + /** + * 发布消息 + * + * @param name 队列名 + * @param key 消息key + * @param data 消息 + * @return 消息id + */ + default RecordId send(String name, String key, byte[] data) { + return send(name, key, data, RedisStreamCommands.XAddOptions.none()); + } + + /** + * 发布消息 + * + * @param name 队列名 + * @param key 消息key + * @param data 消息 + * @param maxLen 限制 stream 最大长度 + * @return 消息id + */ + default RecordId send(String name, String key, byte[] data, long maxLen) { + return send(name, key, data, RedisStreamCommands.XAddOptions.maxlen(maxLen)); + } + + /** + * 发布消息 + * + * @param name 队列名 + * @param key 消息key + * @param data 消息 + * @param options XAddOptions + * @return 消息id + */ + RecordId send(String name, String key, byte[] data, RedisStreamCommands.XAddOptions options); + + /** + * 发布消息 + * + * @param name 队列名 + * @param key 消息key + * @param data 消息 + * @param mapper mapper + * @param 泛型 + * @return 消息id + */ + default RecordId send(String name, String key, T data, Function mapper, long maxLen) { + return send(name, key, mapper.apply(data), maxLen); + } + + /** + * 发布消息 + * + * @param name 队列名 + * @param key 消息key + * @param data 消息 + * @param mapper mapper + * @param options XAddOptions + * @param 泛型 + * @return 消息id + */ + default RecordId send(String name, String key, T data, Function mapper, RedisStreamCommands.XAddOptions options) { + return send(name, key, mapper.apply(data), options); + } + + /** + * 发布消息 + * + * @param name 队列名 + * @param key 消息key + * @param data 消息 + * @param mapper 消息转换 + * @param 泛型 + * @return 消息id + */ + default RecordId send(String name, String key, T data, Function mapper) { + return send(name, key, mapper.apply(data)); + } + + /** + * 批量发布 + * + * @param name 队列名 + * @param messages 消息 + * @return 消息id + */ + default RecordId send(String name, Map messages) { + return send(MapRecord.create(name, messages)); + } + + /** + * 发送消息 + * + * @param record Record + * @return 消息id + */ + RecordId send(Record record); + + /** + * 删除消息 + * + * @param name stream name + * @param recordIds recordIds + * @return Long + */ + @Nullable + Long delete(String name, String... recordIds); + + /** + * 删除消息 + * + * @param name stream name + * @param recordIds recordIds + * @return Long + */ + @Nullable + Long delete(String name, RecordId... recordIds); + + /** + * 删除消息 + * + * @param record Record + * @return Long + */ + @Nullable + default Long delete(Record record) { + return delete(record.getStream(), record.getId()); + } + + /** + * 对流进行修剪,限制长度 + * + * @param name name + * @param count count + * @return Long + */ + @Nullable + default Long trim(String name, long count) { + return trim(name, count, false); + } + + /** + * 对流进行修剪,限制长度 + * + * @param name name + * @param count count + * @param approximateTrimming approximateTrimming + * @return Long + */ + @Nullable + Long trim(String name, long count, boolean approximateTrimming); + + /** + * 手动 ack + * + * @param name name + * @param group group + * @param recordIds recordIds + * @return Long + */ + @Nullable + Long acknowledge(String name, String group, String... recordIds); + + /** + * 手动 ack + * + * @param name name + * @param group group + * @param recordIds recordIds + * @return Long + */ + @Nullable + Long acknowledge(String name, String group, RecordId... recordIds); + + /** + * 手动 ack + * + * @param group group + * @param record record + * @return Long + */ + @Nullable + Long acknowledge(String group, Record record); + +} diff --git a/blade-starter-redis/src/main/java/org/springblade/core/redis/stream/ReadOffsetModel.java b/blade-starter-redis/src/main/java/org/springblade/core/redis/stream/ReadOffsetModel.java new file mode 100644 index 0000000..ccfad3c --- /dev/null +++ b/blade-starter-redis/src/main/java/org/springblade/core/redis/stream/ReadOffsetModel.java @@ -0,0 +1,60 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: DreamLu (596392912@qq.com) + */ + +package org.springblade.core.redis.stream; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.connection.stream.ReadOffset; + +/** + * stream read offset model + * + * @author L.cm + */ +@Getter +@RequiredArgsConstructor +public enum ReadOffsetModel { + + /** + * 从开始的地方读 + */ + START(ReadOffset.from("0-0")), + /** + * 从最近的偏移量读取。 + */ + LATEST(ReadOffset.latest()), + /** + * 读取所有新到达的元素,这些元素的id大于最后一个消费组的id。 + */ + LAST_CONSUMED(ReadOffset.lastConsumed()); + + /** + * readOffset + */ + private final ReadOffset readOffset; + +} diff --git a/blade-starter-redis/src/main/resources/META-INF/scripts/blade_rate_limiter.lua b/blade-starter-redis/src/main/resources/META-INF/scripts/blade_rate_limiter.lua new file mode 100644 index 0000000..2b3d1a2 --- /dev/null +++ b/blade-starter-redis/src/main/resources/META-INF/scripts/blade_rate_limiter.lua @@ -0,0 +1,26 @@ +-- 开启单命令复制模式 +redis.replicate_commands() +-- lua 下标从 1 开始 +-- 限流大小 +local max = tonumber(ARGV[1]) +-- 超时时间 +local ttl = tonumber(ARGV[2]) +local now = redis.call('TIME')[1] +-- 已经过期的时间点 +local expired = now - ttl +-- 清除过期的数据,移除指定分数(score)区间内的所有成员 +redis.call('zremrangebyscore', KEYS[1], 0, expired) +-- 获取当前流量大小 +local currentLimit = tonumber(redis.call('zcard', KEYS[1])) + +local nextLimit = currentLimit + 1 +if nextLimit > max then + -- 达到限流大小 返回 0 + return 0; +else + -- 没有达到阈值 value + 1 + redis.call("zadd", KEYS[1], now, now) + -- 秒为单位设置 key 的生存时间 + redis.call("pexpire", KEYS[1], ttl) + return nextLimit +end diff --git a/blade-starter-redis/src/main/resources/additional-spring-configuration-metadata.json b/blade-starter-redis/src/main/resources/additional-spring-configuration-metadata.json new file mode 100644 index 0000000..c7329f4 --- /dev/null +++ b/blade-starter-redis/src/main/resources/additional-spring-configuration-metadata.json @@ -0,0 +1,11 @@ +{ + "properties": [ + { + "name": "blade.redis.rate-limiter.enabled", + "type": "java.lang.Boolean", + "description": "是否开启 redis 分布式限流.", + "defaultValue": "false" + } + ] +} + diff --git a/blade-starter-report/pom.xml b/blade-starter-report/pom.xml new file mode 100644 index 0000000..59d2a6c --- /dev/null +++ b/blade-starter-report/pom.xml @@ -0,0 +1,62 @@ + + + 4.0.0 + + BladeX-Tool + org.springblade + ${revision} + + + blade-starter-report + ${project.artifactId} + ${project.parent.version} + jar + + + + org.springblade + blade-starter-mybatis + + + com.bstek.ureport + ureport2-console + + + commons-fileupload + commons-fileupload + + + javax.servlet + servlet-api + + + 2.2.9 + + + com.bstek.ureport + ureport2-core + + + javax.servlet + servlet-api + + + 2.2.9 + + + jakarta.servlet + jakarta.servlet-api + 6.0.0 + provided + + + + org.springblade + blade-core-auto + provided + + + + diff --git a/blade-starter-report/src/main/java/com/bstek/ureport/build/Splash.java b/blade-starter-report/src/main/java/com/bstek/ureport/build/Splash.java new file mode 100644 index 0000000..54581d5 --- /dev/null +++ b/blade-starter-report/src/main/java/com/bstek/ureport/build/Splash.java @@ -0,0 +1,47 @@ +/******************************************************************************* + * Copyright 2017 Bstek + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + ******************************************************************************/ +package com.bstek.ureport.build; + +/** + * @author Jacky.gao + * @since 2017年6月19日 + */ +public class Splash { + public void doPrint() { + String sb = "\n" + + " ___ ___ ________ _______ ________ ________ ________ _________ ________ \n" + + "|\\ \\|\\ \\|\\ __ \\|\\ ___ \\ |\\ __ \\|\\ __ \\|\\ __ \\|\\___ ___\\ |\\_____ \\ \n" + + "\\ \\ \\\\\\ \\ \\ \\|\\ \\ \\ __/|\\ \\ \\|\\ \\ \\ \\|\\ \\ \\ \\|\\ \\|___ \\ \\_| \\|____|\\ /_ \n" + + " \\ \\ \\\\\\ \\ \\ _ _\\ \\ \\_|/_\\ \\ ____\\ \\ \\\\\\ \\ \\ _ _\\ \\ \\ \\ \\|\\ \\ \n" + + " \\ \\ \\\\\\ \\ \\ \\\\ \\\\ \\ \\_|\\ \\ \\ \\___|\\ \\ \\\\\\ \\ \\ \\\\ \\| \\ \\ \\ __\\_\\ \\ \n" + + " \\ \\_______\\ \\__\\\\ _\\\\ \\_______\\ \\__\\ \\ \\_______\\ \\__\\\\ _\\ \\ \\__\\ |\\_______\\\n" + + " \\|_______|\\|__|\\|__|\\|_______|\\|__| \\|_______|\\|__|\\|__| \\|__| \\|_______|\n" + + "........................................................................................................" + + "\n" + + ". uReport, is a Chinese style report engine" + + " licensed under the Apache License 2.0, ." + + "\n" + + ". which is opensource, easy to use,high-performance, with browser-based-designer, ." + + "\n" + + ". it has now been upgraded by BladeX to support jdk 17 and spring boot 3. ." + + "\n" + + "........................................................................................................" + + "\n"; + System.out.println(sb); + } + + +} diff --git a/blade-starter-report/src/main/java/com/bstek/ureport/build/compute/ZxingValueCompute.java b/blade-starter-report/src/main/java/com/bstek/ureport/build/compute/ZxingValueCompute.java new file mode 100644 index 0000000..528edc4 --- /dev/null +++ b/blade-starter-report/src/main/java/com/bstek/ureport/build/compute/ZxingValueCompute.java @@ -0,0 +1,150 @@ +/******************************************************************************* + * Copyright 2017 Bstek + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + ******************************************************************************/ +package com.bstek.ureport.build.compute; + +import com.bstek.ureport.build.BindData; +import com.bstek.ureport.build.Context; +import com.bstek.ureport.definition.value.Source; +import com.bstek.ureport.definition.value.ValueType; +import com.bstek.ureport.definition.value.ZxingValue; +import com.bstek.ureport.exception.ReportComputeException; +import com.bstek.ureport.expression.model.Expression; +import com.bstek.ureport.expression.model.data.BindDataListExpressionData; +import com.bstek.ureport.expression.model.data.ExpressionData; +import com.bstek.ureport.expression.model.data.ObjectExpressionData; +import com.bstek.ureport.expression.model.data.ObjectListExpressionData; +import com.bstek.ureport.model.Cell; +import com.bstek.ureport.model.Image; +import com.google.zxing.BarcodeFormat; +import com.google.zxing.EncodeHintType; +import com.google.zxing.MultiFormatWriter; +import com.google.zxing.common.BitMatrix; +import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.springblade.core.tool.utils.Base64Util; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.util.ArrayList; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; + +/** + * @author Jacky.gao + * @since 2017年3月27日 + */ +public class ZxingValueCompute implements ValueCompute { + private static final int BLACK = 0xff000000; + private static final int WHITE = 0xFFFFFFFF; + @Override + public List compute(Cell cell, Context context) { + List list=new ArrayList(); + ZxingValue value=(ZxingValue)cell.getValue(); + String format=value.getFormat(); + BarcodeFormat barcodeForamt=BarcodeFormat.QR_CODE; + if(StringUtils.isNotBlank(format)){ + barcodeForamt=BarcodeFormat.valueOf(format); + } + int w=value.getWidth(); + int h=value.getHeight(); + Source source=value.getSource(); + if(source.equals(Source.text)){ + String data=value.getValue(); + Image image=buildImage(barcodeForamt,data,w,h); + list.add(new BindData(image)); + }else{ + Expression expression=value.getExpression(); + ExpressionData data=expression.execute(cell,cell, context); + if(data instanceof BindDataListExpressionData){ + BindDataListExpressionData listData=(BindDataListExpressionData)data; + List bindDataList=listData.getData(); + for(BindData bindData:bindDataList){ + Object obj=bindData.getValue(); + if(obj==null)obj=""; + Image image=buildImage(barcodeForamt,obj.toString(),w,h); + list.add(new BindData(image)); + } + }else if(data instanceof ObjectExpressionData){ + ObjectExpressionData exprData=(ObjectExpressionData)data; + Object obj=exprData.getData(); + if(obj==null){ + obj=""; + }else if(obj instanceof String){ + String text=obj.toString(); + if(text.startsWith("\"") && text.endsWith("\"")){ + text=text.substring(1,text.length()-1); + } + obj=text; + } + Image image=buildImage(barcodeForamt,obj.toString(),w,h); + list.add(new BindData(image)); + }else if(data instanceof ObjectListExpressionData){ + ObjectListExpressionData listExprData=(ObjectListExpressionData)data; + List listData=listExprData.getData(); + for(Object obj:listData){ + if(obj==null){ + obj=""; + }else if(obj instanceof String){ + String text=obj.toString(); + if(text.startsWith("\"") && text.endsWith("\"")){ + text=text.substring(1,text.length()-1); + } + obj=text; + } + Image image=buildImage(barcodeForamt,obj.toString(),w,h); + list.add(new BindData(image)); + } + } + } + return list; + } + + private Image buildImage(BarcodeFormat format,String data,int w,int h){ + try{ + Map hints = new Hashtable(); + hints.put(EncodeHintType.CHARACTER_SET, "utf-8"); + hints.put(EncodeHintType.MARGIN,0); + if(format.equals(BarcodeFormat.QR_CODE)){ + hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H); + } + BitMatrix matrix = new MultiFormatWriter().encode(data,format, w, h,hints); + int width = matrix.getWidth(); + int height = matrix.getHeight(); + BufferedImage image = new BufferedImage(width, height,BufferedImage.TYPE_INT_ARGB); + for (int x = 0; x < width; x++) { + for (int y = 0; y < height; y++) { + image.setRGB(x, y, matrix.get(x, y) ? BLACK : WHITE); + } + } + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ImageIO.write(image, "png", outputStream); + byte[] bytes=outputStream.toByteArray(); + String base64Data= Base64Util.encodeToString(bytes); + IOUtils.closeQuietly(outputStream); + return new Image(base64Data,w,h); + }catch(Exception ex){ + throw new ReportComputeException(ex); + } + } + + @Override + public ValueType type() { + return ValueType.zxing; + } +} diff --git a/blade-starter-report/src/main/java/com/bstek/ureport/console/BaseServletAction.java b/blade-starter-report/src/main/java/com/bstek/ureport/console/BaseServletAction.java new file mode 100644 index 0000000..e4a1eae --- /dev/null +++ b/blade-starter-report/src/main/java/com/bstek/ureport/console/BaseServletAction.java @@ -0,0 +1,125 @@ +/******************************************************************************* + * Copyright 2017 Bstek + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + ******************************************************************************/ +package com.bstek.ureport.console; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.apache.commons.lang3.StringUtils; + +import java.lang.reflect.Method; +import java.net.URLDecoder; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; + + +/** + * @author Jacky.gao + * @since 2016年6月3日 + */ +public abstract class BaseServletAction implements ServletAction { + protected Throwable buildRootException(Throwable throwable) { + if (throwable.getCause() == null) { + return throwable; + } + return buildRootException(throwable.getCause()); + } + + protected String decode(String value) { + if (value == null) { + return value; + } + try { + value = URLDecoder.decode(value, "utf-8"); + value = URLDecoder.decode(value, "utf-8"); + return value; + } catch (Exception ex) { + return value; + } + } + + protected String decodeContent(String content) { + if (content == null) { + return content; + } + try { + content = URLDecoder.decode(content, "utf-8"); + return content; + } catch (Exception ex) { + return content; + } + } + + protected Map buildParameters(HttpServletRequest req) { + Map parameters = new HashMap(); + Enumeration enumeration = req.getParameterNames(); + while (enumeration.hasMoreElements()) { + Object obj = enumeration.nextElement(); + if (obj == null) { + continue; + } + String name = obj.toString(); + String value = req.getParameter(name); + if (name == null || value == null || name.startsWith("_")) { + continue; + } + parameters.put(name, decode(value)); + } + return parameters; + } + + protected void invokeMethod(String methodName, HttpServletRequest req, HttpServletResponse resp) throws ServletException { + try { + Method method = this.getClass().getMethod(methodName, new Class[]{HttpServletRequest.class, HttpServletResponse.class}); + method.invoke(this, new Object[]{req, resp}); + } catch (Exception ex) { + throw new ServletException(ex); + } + } + + protected String retriveMethod(HttpServletRequest req) throws ServletException { + String path = req.getContextPath() + UReportServlet.URL; + String uri = req.getRequestURI(); + String targetUrl = uri.substring(path.length()); + int slashPos = targetUrl.indexOf("/", 1); + if (slashPos > -1) { + String methodName = targetUrl.substring(slashPos + 1).trim(); + return methodName.length() > 0 ? methodName : null; + } + return null; + } + + protected String buildDownloadFileName(String reportFileName, String fileName, String extName) { + if (StringUtils.isNotBlank(fileName)) { + fileName = decode(fileName); + if (!fileName.toLowerCase().endsWith(extName)) { + fileName = fileName + extName; + } + return fileName; + } else { + int pos = reportFileName.indexOf(":"); + if (pos > 0) { + reportFileName = reportFileName.substring(pos + 1, reportFileName.length()); + } + pos = reportFileName.toLowerCase().indexOf(".ureport.xml"); + if (pos > 0) { + reportFileName = reportFileName.substring(0, pos); + } + return "ureport-" + reportFileName + extName; + } + } +} diff --git a/blade-starter-report/src/main/java/com/bstek/ureport/console/MobileUtils.java b/blade-starter-report/src/main/java/com/bstek/ureport/console/MobileUtils.java new file mode 100644 index 0000000..ac96dfb --- /dev/null +++ b/blade-starter-report/src/main/java/com/bstek/ureport/console/MobileUtils.java @@ -0,0 +1,52 @@ +/******************************************************************************* + * Copyright 2017 Bstek + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + ******************************************************************************/ +package com.bstek.ureport.console; + +import jakarta.servlet.http.HttpServletRequest; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * @author Jacky.gao + * @since 2017年10月11日 + */ +public class MobileUtils { + private static String phoneReg = "\\b(ip(hone|od)|android|opera m(ob|in)i" + + "|windows (phone|ce)|blackberry" + + "|s(ymbian|eries60|amsung)|p(laybook|alm|rofile/midp" + + "|laystation portable)|nokia|fennec|htc[-_]" + + "|mobile|up.browser|[1-4][0-9]{2}x[1-4][0-9]{2})\\b"; + private static String tableReg = "\\b(ipad|tablet|(Nexus 7)|up.browser" + + "|[1-4][0-9]{2}x[1-4][0-9]{2})\\b"; + private static Pattern phonePat = Pattern.compile(phoneReg, Pattern.CASE_INSENSITIVE); + private static Pattern tablePat = Pattern.compile(tableReg, Pattern.CASE_INSENSITIVE); + + public static boolean isMobile(HttpServletRequest req) { + String userAgent = req.getHeader("USER-AGENT"); + if (userAgent == null) { + userAgent = ""; + } + userAgent = userAgent.toLowerCase(); + Matcher matcherPhone = phonePat.matcher(userAgent); + Matcher matcherTable = tablePat.matcher(userAgent); + if (matcherPhone.find() || matcherTable.find()) { + return true; + } else { + return false; + } + } +} diff --git a/blade-starter-report/src/main/java/com/bstek/ureport/console/RequestHolder.java b/blade-starter-report/src/main/java/com/bstek/ureport/console/RequestHolder.java new file mode 100644 index 0000000..c45ff57 --- /dev/null +++ b/blade-starter-report/src/main/java/com/bstek/ureport/console/RequestHolder.java @@ -0,0 +1,38 @@ +/******************************************************************************* + * Copyright 2017 Bstek + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + ******************************************************************************/ +package com.bstek.ureport.console; + +import jakarta.servlet.http.HttpServletRequest; + +/** + * @author Jacky.gao + * @since 2017年3月8日 + */ +public class RequestHolder { + private static final ThreadLocal requestThreadLocal = new ThreadLocal(); + + public static void setRequest(HttpServletRequest request) { + requestThreadLocal.set(request); + } + + public static HttpServletRequest getRequest() { + return requestThreadLocal.get(); + } + + public static void clean() { + requestThreadLocal.remove(); + } +} diff --git a/blade-starter-report/src/main/java/com/bstek/ureport/console/ServletAction.java b/blade-starter-report/src/main/java/com/bstek/ureport/console/ServletAction.java new file mode 100644 index 0000000..3ddb64f --- /dev/null +++ b/blade-starter-report/src/main/java/com/bstek/ureport/console/ServletAction.java @@ -0,0 +1,34 @@ +/******************************************************************************* + * Copyright 2017 Bstek + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + ******************************************************************************/ +package com.bstek.ureport.console; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +/** + * @author Jacky.gao + * @since 2017年1月25日 + */ +public interface ServletAction { + public static final String PREVIEW_KEY = "p"; + + void execute(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException; + + String url(); +} diff --git a/blade-starter-report/src/main/java/com/bstek/ureport/console/UReportServlet.java b/blade-starter-report/src/main/java/com/bstek/ureport/console/UReportServlet.java new file mode 100644 index 0000000..c9670b4 --- /dev/null +++ b/blade-starter-report/src/main/java/com/bstek/ureport/console/UReportServlet.java @@ -0,0 +1,117 @@ +/******************************************************************************* + * Copyright 2017 Bstek + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + ******************************************************************************/ +package com.bstek.ureport.console; + +import jakarta.servlet.ServletConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.apache.commons.lang.StringUtils; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.support.WebApplicationContextUtils; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +/** + * @author Jacky.gao + * @since 2017年1月25日 + */ +public class UReportServlet extends HttpServlet { + private static final long serialVersionUID = 533049461276487971L; + public static final String URL = "/ureport"; + private Map actionMap = new HashMap(); + + @Override + public void init(ServletConfig config) throws ServletException { + super.init(config); + WebApplicationContext applicationContext = getWebApplicationContext(config); + Collection handlers = applicationContext.getBeansOfType(ServletAction.class).values(); + for (ServletAction handler : handlers) { + String url = handler.url(); + if (actionMap.containsKey(url)) { + throw new RuntimeException("Handler [" + url + "] already exist."); + } + actionMap.put(url, handler); + } + } + + protected WebApplicationContext getWebApplicationContext(ServletConfig config) { + return WebApplicationContextUtils.getWebApplicationContext(config.getServletContext()); + } + + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + String path = req.getContextPath() + URL; + String uri = req.getRequestURI(); + String targetUrl = uri.substring(path.length()); + if (targetUrl.length() < 1) { + outContent(resp, "Welcome to use ureport,please specify target url."); + return; + } + int slashPos = targetUrl.indexOf("/", 1); + if (slashPos > -1) { + targetUrl = targetUrl.substring(0, slashPos); + } + ServletAction targetHandler = actionMap.get(targetUrl); + if (targetHandler == null) { + outContent(resp, "Handler [" + targetUrl + "] not exist."); + return; + } + RequestHolder.setRequest(req); + try { + targetHandler.execute(req, resp); + } catch (Exception ex) { + resp.setCharacterEncoding("UTF-8"); + PrintWriter pw = resp.getWriter(); + Throwable e = buildRootException(ex); + resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + String errorMsg = e.getMessage(); + if (StringUtils.isBlank(errorMsg)) { + errorMsg = e.getClass().getName(); + } + pw.write(errorMsg); + pw.close(); + throw new ServletException(ex); + } finally { + RequestHolder.clean(); + } + } + + private Throwable buildRootException(Throwable throwable) { + if (throwable.getCause() == null) { + return throwable; + } + return buildRootException(throwable.getCause()); + } + + private void outContent(HttpServletResponse resp, String msg) throws IOException { + resp.setContentType("text/html"); + PrintWriter pw = resp.getWriter(); + pw.write(""); + pw.write("

UReport Console
"); + pw.write(""); + pw.write(msg); + pw.write(""); + pw.write(""); + pw.flush(); + pw.close(); + } +} diff --git a/blade-starter-report/src/main/java/com/bstek/ureport/console/WriteJsonServletAction.java b/blade-starter-report/src/main/java/com/bstek/ureport/console/WriteJsonServletAction.java new file mode 100644 index 0000000..f94c4d2 --- /dev/null +++ b/blade-starter-report/src/main/java/com/bstek/ureport/console/WriteJsonServletAction.java @@ -0,0 +1,49 @@ +/******************************************************************************* + * Copyright 2017 Bstek + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + ******************************************************************************/ +package com.bstek.ureport.console; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletResponse; +import org.codehaus.jackson.map.ObjectMapper; +import org.codehaus.jackson.map.SerializationConfig; +import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion; + +import java.io.IOException; +import java.io.OutputStream; +import java.text.SimpleDateFormat; + + +/** + * @author Jacky.gao + * @since 2016年5月23日 + */ +public abstract class WriteJsonServletAction extends BaseServletAction { + protected void writeObjectToJson(HttpServletResponse resp, Object obj) throws ServletException, IOException { + resp.setContentType("text/json"); + resp.setCharacterEncoding("UTF-8"); + ObjectMapper mapper = new ObjectMapper(); + mapper.setSerializationInclusion(Inclusion.NON_NULL); + mapper.configure(SerializationConfig.Feature.WRITE_DATES_AS_TIMESTAMPS, false); + mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); + OutputStream out = resp.getOutputStream(); + try { + mapper.writeValue(out, obj); + } finally { + out.flush(); + out.close(); + } + } +} diff --git a/blade-starter-report/src/main/java/com/bstek/ureport/console/cache/HttpSessionReportCache.java b/blade-starter-report/src/main/java/com/bstek/ureport/console/cache/HttpSessionReportCache.java new file mode 100644 index 0000000..a905d5d --- /dev/null +++ b/blade-starter-report/src/main/java/com/bstek/ureport/console/cache/HttpSessionReportCache.java @@ -0,0 +1,85 @@ +/******************************************************************************* + * Copyright 2017 Bstek + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + ******************************************************************************/ +package com.bstek.ureport.console.cache; + +import com.bstek.ureport.cache.ReportCache; +import com.bstek.ureport.console.RequestHolder; +import jakarta.servlet.http.HttpServletRequest; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author Jacky.gao + * @since 2017年3月8日 + */ +public class HttpSessionReportCache implements ReportCache { + private Map sessionReportMap = new HashMap(); + private boolean disabled; + + @Override + public Object getObject(String file) { + HttpServletRequest req = RequestHolder.getRequest(); + if (req == null) { + return null; + } + ObjectMap objMap = getObjectMap(req); + return objMap.get(file); + } + + @Override + public void storeObject(String file, Object object) { + HttpServletRequest req = RequestHolder.getRequest(); + if (req == null) { + return; + } + ObjectMap map = getObjectMap(req); + map.put(file, object); + } + + @Override + public boolean disabled() { + return disabled; + } + + public void setDisabled(boolean disabled) { + this.disabled = disabled; + } + + private ObjectMap getObjectMap(HttpServletRequest req) { + List expiredList = new ArrayList(); + for (String key : sessionReportMap.keySet()) { + ObjectMap reportObj = sessionReportMap.get(key); + if (reportObj.isExpired()) { + expiredList.add(key); + } + } + for (String key : expiredList) { + sessionReportMap.remove(key); + } + String sessionId = req.getSession().getId(); + ObjectMap obj = sessionReportMap.get(sessionId); + if (obj != null) { + return obj; + } else { + ObjectMap objMap = new ObjectMap(); + sessionReportMap.put(sessionId, objMap); + return objMap; + } + } +} diff --git a/blade-starter-report/src/main/java/com/bstek/ureport/console/cache/TempObjectCache.java b/blade-starter-report/src/main/java/com/bstek/ureport/console/cache/TempObjectCache.java new file mode 100644 index 0000000..8d3859d --- /dev/null +++ b/blade-starter-report/src/main/java/com/bstek/ureport/console/cache/TempObjectCache.java @@ -0,0 +1,97 @@ +/******************************************************************************* + * Copyright 2017 Bstek + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + ******************************************************************************/ +package com.bstek.ureport.console.cache; + +import com.bstek.ureport.console.RequestHolder; +import jakarta.servlet.http.HttpServletRequest; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + + +/** + * @author Jacky.gao + * @since 2017年9月6日 + */ +public class TempObjectCache { + private static TempObjectCache tempObjectCache = new TempObjectCache(); + private Map sessionMap = new HashMap(); + + public static Object getObject(String key) { + return tempObjectCache.get(key); + } + + public static void putObject(String key, Object obj) { + tempObjectCache.store(key, obj); + } + + public static void removeObject(String key) { + tempObjectCache.remove(key); + } + + public void remove(String key) { + HttpServletRequest req = RequestHolder.getRequest(); + if (req == null) { + return; + } + ObjectMap mapObject = getReportMap(req); + if (mapObject != null) { + mapObject.remove(key); + } + } + + public Object get(String key) { + HttpServletRequest req = RequestHolder.getRequest(); + if (req == null) { + return null; + } + ObjectMap mapObject = getReportMap(req); + return mapObject.get(key); + } + + public void store(String key, Object obj) { + HttpServletRequest req = RequestHolder.getRequest(); + if (req == null) { + return; + } + ObjectMap mapObject = getReportMap(req); + mapObject.put(key, obj); + } + + private ObjectMap getReportMap(HttpServletRequest req) { + List expiredList = new ArrayList(); + for (String key : sessionMap.keySet()) { + ObjectMap reportObj = sessionMap.get(key); + if (reportObj.isExpired()) { + expiredList.add(key); + } + } + for (String key : expiredList) { + sessionMap.remove(key); + } + String sessionId = req.getSession().getId(); + ObjectMap obj = sessionMap.get(sessionId); + if (obj != null) { + return obj; + } else { + ObjectMap mapObject = new ObjectMap(); + sessionMap.put(sessionId, mapObject); + return mapObject; + } + } +} diff --git a/blade-starter-report/src/main/java/com/bstek/ureport/console/chart/ChartServletAction.java b/blade-starter-report/src/main/java/com/bstek/ureport/console/chart/ChartServletAction.java new file mode 100644 index 0000000..16117fd --- /dev/null +++ b/blade-starter-report/src/main/java/com/bstek/ureport/console/chart/ChartServletAction.java @@ -0,0 +1,67 @@ +/******************************************************************************* + * Copyright 2017 Bstek + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + ******************************************************************************/ +package com.bstek.ureport.console.chart; + +import com.bstek.ureport.cache.CacheUtils; +import com.bstek.ureport.chart.ChartData; +import com.bstek.ureport.console.RenderPageServletAction; +import com.bstek.ureport.utils.UnitUtils; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +/** + * @author Jacky.gao + * @since 2017年6月30日 + */ +public class ChartServletAction extends RenderPageServletAction { + @Override + public void execute(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + String method = retriveMethod(req); + if (method != null) { + invokeMethod(method, req, resp); + } + } + + public void storeData(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + String file = req.getParameter("_u"); + file = decode(file); + String chartId = req.getParameter("_chartId"); + ChartData chartData = CacheUtils.getChartData(chartId); + if (chartData == null) { + return; + } + String base64Data = req.getParameter("_base64Data"); + String prefix = "data:image/png;base64,"; + if (base64Data != null) { + if (base64Data.startsWith(prefix)) { + base64Data = base64Data.substring(prefix.length(), base64Data.length()); + } + } + chartData.setBase64Data(base64Data); + String width = req.getParameter("_width"); + String height = req.getParameter("_height"); + chartData.setHeight(UnitUtils.pixelToPoint(Integer.valueOf(height))); + chartData.setWidth(UnitUtils.pixelToPoint(Integer.valueOf(width))); + } + + @Override + public String url() { + return "/chart"; + } +} diff --git a/blade-starter-report/src/main/java/com/bstek/ureport/console/designer/DatasourceServletAction.java b/blade-starter-report/src/main/java/com/bstek/ureport/console/designer/DatasourceServletAction.java new file mode 100644 index 0000000..779c7b2 --- /dev/null +++ b/blade-starter-report/src/main/java/com/bstek/ureport/console/designer/DatasourceServletAction.java @@ -0,0 +1,385 @@ +/******************************************************************************* + * Copyright 2017 Bstek + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + ******************************************************************************/ +package com.bstek.ureport.console.designer; + +import com.bstek.ureport.Utils; +import com.bstek.ureport.build.Context; +import com.bstek.ureport.console.RenderPageServletAction; +import com.bstek.ureport.console.exception.ReportDesignException; +import com.bstek.ureport.definition.dataset.Field; +import com.bstek.ureport.definition.datasource.BuildinDatasource; +import com.bstek.ureport.definition.datasource.DataType; +import com.bstek.ureport.expression.ExpressionUtils; +import com.bstek.ureport.expression.model.Expression; +import com.bstek.ureport.expression.model.data.ExpressionData; +import com.bstek.ureport.expression.model.data.ObjectExpressionData; +import com.bstek.ureport.utils.ProcedureUtils; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.apache.commons.beanutils.PropertyUtils; +import org.apache.commons.lang3.StringUtils; +import org.codehaus.jackson.JsonParseException; +import org.codehaus.jackson.map.JsonMappingException; +import org.codehaus.jackson.map.ObjectMapper; +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.core.PreparedStatementCallback; +import org.springframework.jdbc.core.PreparedStatementCreator; +import org.springframework.jdbc.core.PreparedStatementCreatorFactory; +import org.springframework.jdbc.core.SqlParameter; +import org.springframework.jdbc.core.namedparam.*; +import org.springframework.jdbc.datasource.SingleConnectionDataSource; +import org.springframework.jdbc.support.JdbcUtils; + +import javax.sql.DataSource; +import java.beans.PropertyDescriptor; +import java.io.IOException; +import java.lang.reflect.Method; +import java.sql.*; +import java.util.Date; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * @author Jacky.gao + * @since 2017年2月6日 + */ +public class DatasourceServletAction extends RenderPageServletAction { + + @Override + public void execute(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + String method = retriveMethod(req); + if (method != null) { + invokeMethod(method, req, resp); + } + } + + public void loadBuildinDatasources(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + List datasources = new ArrayList(); + for (BuildinDatasource datasource : Utils.getBuildinDatasources()) { + datasources.add(datasource.name()); + } + writeObjectToJson(resp, datasources); + } + + public void loadMethods(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + String beanId = req.getParameter("beanId"); + Object obj = applicationContext.getBean(beanId); + Class clazz = obj.getClass(); + Method[] methods = clazz.getMethods(); + List result = new ArrayList(); + for (Method method : methods) { + Class[] types = method.getParameterTypes(); + if (types.length != 3) { + continue; + } + Class typeClass1 = types[0]; + Class typeClass2 = types[1]; + Class typeClass3 = types[2]; + if (!String.class.isAssignableFrom(typeClass1)) { + continue; + } + if (!String.class.isAssignableFrom(typeClass2)) { + continue; + } + if (!Map.class.isAssignableFrom(typeClass3)) { + continue; + } + result.add(method.getName()); + } + writeObjectToJson(resp, result); + } + + public void buildClass(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + String clazz = req.getParameter("clazz"); + List result = new ArrayList(); + try { + Class targetClass = Class.forName(clazz); + PropertyDescriptor[] propertyDescriptors = PropertyUtils.getPropertyDescriptors(targetClass); + for (PropertyDescriptor pd : propertyDescriptors) { + String name = pd.getName(); + if ("class".equals(name)) { + continue; + } + result.add(new Field(name)); + } + writeObjectToJson(resp, result); + } catch (Exception ex) { + throw new ReportDesignException(ex); + } + } + + public void buildDatabaseTables(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + Connection conn = null; + ResultSet rs = null; + try { + conn = buildConnection(req); + DatabaseMetaData metaData = conn.getMetaData(); + String url = metaData.getURL(); + String schema = null; + if (url.toLowerCase().contains("oracle")) { + schema = metaData.getUserName(); + } + List> tables = new ArrayList>(); + rs = metaData.getTables(null, schema, "%", new String[]{"TABLE", "VIEW"}); + while (rs.next()) { + Map table = new HashMap(); + table.put("name", rs.getString("TABLE_NAME")); + table.put("type", rs.getString("TABLE_TYPE")); + tables.add(table); + } + writeObjectToJson(resp, tables); + } catch (Exception ex) { + throw new ServletException(ex); + } finally { + JdbcUtils.closeResultSet(rs); + JdbcUtils.closeConnection(conn); + } + } + + public void buildFields(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + String sql = req.getParameter("sql"); + String parameters = req.getParameter("parameters"); + Connection conn = null; + final List fields = new ArrayList(); + try { + conn = buildConnection(req); + Map map = buildParameters(parameters); + sql = parseSql(sql, map); + if (ProcedureUtils.isProcedure(sql)) { + List fieldsList = ProcedureUtils.procedureColumnsQuery(sql, map, conn); + fields.addAll(fieldsList); + } else { + DataSource dataSource = new SingleConnectionDataSource(conn, false); + NamedParameterJdbcTemplate jdbc = new NamedParameterJdbcTemplate(dataSource); + PreparedStatementCreator statementCreator = getPreparedStatementCreator(sql, new MapSqlParameterSource(map)); + jdbc.getJdbcOperations().execute(statementCreator, new PreparedStatementCallback() { + @Override + public Object doInPreparedStatement(PreparedStatement ps) throws SQLException, DataAccessException { + ResultSet rs = null; + try { + rs = ps.executeQuery(); + ResultSetMetaData metadata = rs.getMetaData(); + int columnCount = metadata.getColumnCount(); + for (int i = 0; i < columnCount; i++) { + String columnName = metadata.getColumnLabel(i + 1); + fields.add(new Field(columnName)); + } + return null; + } finally { + JdbcUtils.closeResultSet(rs); + } + } + }); + } + writeObjectToJson(resp, fields); + } catch (Exception ex) { + throw new ReportDesignException(ex); + } finally { + JdbcUtils.closeConnection(conn); + } + } + + protected PreparedStatementCreator getPreparedStatementCreator(String sql, SqlParameterSource paramSource) { + ParsedSql parsedSql = NamedParameterUtils.parseSqlStatement(sql); + String sqlToUse = NamedParameterUtils.substituteNamedParameters(parsedSql, paramSource); + Object[] params = NamedParameterUtils.buildValueArray(parsedSql, paramSource, null); + List declaredParameters = NamedParameterUtils.buildSqlParameterList(parsedSql, paramSource); + PreparedStatementCreatorFactory pscf = new PreparedStatementCreatorFactory(sqlToUse, declaredParameters); + return pscf.newPreparedStatementCreator(params); + } + + public void previewData(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + String sql = req.getParameter("sql"); + String parameters = req.getParameter("parameters"); + Map map = buildParameters(parameters); + sql = parseSql(sql, map); + Connection conn = null; + try { + conn = buildConnection(req); + List> list = null; + if (ProcedureUtils.isProcedure(sql)) { + list = ProcedureUtils.procedureQuery(sql, map, conn); + } else { + DataSource dataSource = new SingleConnectionDataSource(conn, false); + NamedParameterJdbcTemplate jdbc = new NamedParameterJdbcTemplate(dataSource); + list = jdbc.queryForList(sql, map); + } + int size = list.size(); + int currentTotal = size; + if (currentTotal > 500) { + currentTotal = 500; + } + List> ls = new ArrayList>(); + for (int i = 0; i < currentTotal; i++) { + ls.add(list.get(i)); + } + DataResult result = new DataResult(); + List fields = new ArrayList(); + if (size > 0) { + Map item = list.get(0); + for (String name : item.keySet()) { + fields.add(name); + } + } + result.setFields(fields); + result.setCurrentTotal(currentTotal); + result.setData(ls); + result.setTotal(size); + writeObjectToJson(resp, result); + } catch (Exception ex) { + throw new ServletException(ex); + } finally { + if (conn != null) { + try { + conn.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + } + } + + private String parseSql(String sql, Map parameters) { + sql = sql.trim(); + Context context = new Context(applicationContext, parameters); + if (sql.startsWith(ExpressionUtils.EXPR_PREFIX) && sql.endsWith(ExpressionUtils.EXPR_SUFFIX)) { + sql = sql.substring(2, sql.length() - 1); + Expression expr = ExpressionUtils.parseExpression(sql); + sql = executeSqlExpr(expr, context); + return sql; + } else { + String sqlForUse = sql; + Pattern pattern = Pattern.compile("\\$\\{.*?\\}"); + Matcher matcher = pattern.matcher(sqlForUse); + while (matcher.find()) { + String substr = matcher.group(); + String sqlExpr = substr.substring(2, substr.length() - 1); + Expression expr = ExpressionUtils.parseExpression(sqlExpr); + String result = executeSqlExpr(expr, context); + sqlForUse = sqlForUse.replace(substr, result); + } + Utils.logToConsole("DESIGN SQL:" + sqlForUse); + return sqlForUse; + } + } + + private String executeSqlExpr(Expression sqlExpr, Context context) { + String sqlForUse = null; + ExpressionData exprData = sqlExpr.execute(null, null, context); + if (exprData instanceof ObjectExpressionData) { + ObjectExpressionData data = (ObjectExpressionData) exprData; + Object obj = data.getData(); + if (obj != null) { + String s = obj.toString(); + s = s.replaceAll("\\\\", ""); + sqlForUse = s; + } + } + return sqlForUse; + } + + public void testConnection(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + String username = req.getParameter("username"); + String password = req.getParameter("password"); + String driver = req.getParameter("driver"); + String url = req.getParameter("url"); + Connection conn = null; + Map map = new HashMap(); + try { + Class.forName(driver); + conn = DriverManager.getConnection(url, username, password); + map.put("result", true); + } catch (Exception ex) { + map.put("error", ex.toString()); + map.put("result", false); + } finally { + if (conn != null) { + try { + conn.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + } + writeObjectToJson(resp, map); + } + + @SuppressWarnings("unchecked") + private Map buildParameters(String parameters) throws IOException, JsonParseException, JsonMappingException { + Map map = new HashMap(); + if (StringUtils.isBlank(parameters)) { + return map; + } + ObjectMapper mapper = new ObjectMapper(); + List> list = mapper.readValue(parameters, ArrayList.class); + for (Map param : list) { + String name = param.get("name").toString(); + DataType type = DataType.valueOf(param.get("type").toString()); + String defaultValue = (String) param.get("defaultValue"); + if (defaultValue == null || defaultValue.isEmpty()) { + switch (type) { + case Boolean: + map.put(name, false); + case Date: + map.put(name, new Date()); + case Float: + map.put(name, (float) 0); + case Integer: + map.put(name, 0); + case String: + if (defaultValue != null) { + map.put(name, ""); + } else { + map.put(name, "null"); + } + break; + case List: + map.put(name, new ArrayList<>()); + } + } else { + map.put(name, type.parse(defaultValue)); + } + } + return map; + } + + private Connection buildConnection(HttpServletRequest req) throws Exception { + String type = req.getParameter("type"); + if (type.equals("jdbc")) { + String username = req.getParameter("username"); + String password = req.getParameter("password"); + String driver = req.getParameter("driver"); + String url = req.getParameter("url"); + + Class.forName(driver); + return DriverManager.getConnection(url, username, password); + } else { + String name = req.getParameter("name"); + Connection conn = Utils.getBuildinConnection(name); + if (conn == null) { + throw new ReportDesignException("Buildin datasource [" + name + "] not exist."); + } + return conn; + } + } + + @Override + public String url() { + return "/datasource"; + } +} diff --git a/blade-starter-report/src/main/java/com/bstek/ureport/console/designer/DesignerServletAction.java b/blade-starter-report/src/main/java/com/bstek/ureport/console/designer/DesignerServletAction.java new file mode 100644 index 0000000..76fa50d --- /dev/null +++ b/blade-starter-report/src/main/java/com/bstek/ureport/console/designer/DesignerServletAction.java @@ -0,0 +1,213 @@ +/******************************************************************************* + * Copyright 2017 Bstek + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + ******************************************************************************/ +package com.bstek.ureport.console.designer; + +import com.bstek.ureport.cache.CacheUtils; +import com.bstek.ureport.console.RenderPageServletAction; +import com.bstek.ureport.console.cache.TempObjectCache; +import com.bstek.ureport.console.exception.ReportDesignException; +import com.bstek.ureport.definition.ReportDefinition; +import com.bstek.ureport.dsl.ReportParserLexer; +import com.bstek.ureport.dsl.ReportParserParser; +import com.bstek.ureport.dsl.ReportParserParser.DatasetContext; +import com.bstek.ureport.export.ReportRender; +import com.bstek.ureport.expression.ErrorInfo; +import com.bstek.ureport.expression.ScriptErrorListener; +import com.bstek.ureport.parser.ReportParser; +import com.bstek.ureport.provider.report.ReportProvider; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.antlr.v4.runtime.ANTLRInputStream; +import org.antlr.v4.runtime.CommonTokenStream; +import org.apache.commons.io.IOUtils; +import org.apache.velocity.Template; +import org.apache.velocity.VelocityContext; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; + +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintWriter; +import java.util.*; + +/** + * @author Jacky.gao + * @since 2017年1月25日 + */ +public class DesignerServletAction extends RenderPageServletAction { + private ReportRender reportRender; + private ReportParser reportParser; + private List reportProviders = new ArrayList(); + + @Override + public void execute(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + String method = retriveMethod(req); + if (method != null) { + invokeMethod(method, req, resp); + } else { + VelocityContext context = new VelocityContext(); + context.put("contextPath", req.getContextPath()); + resp.setContentType("text/html"); + resp.setCharacterEncoding("utf-8"); + Template template = ve.getTemplate("ureport-html/designer.html", "utf-8"); + PrintWriter writer = resp.getWriter(); + template.merge(context, writer); + writer.close(); + } + } + + public void scriptValidation(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + String content = req.getParameter("content"); + ANTLRInputStream antlrInputStream = new ANTLRInputStream(content); + ReportParserLexer lexer = new ReportParserLexer(antlrInputStream); + CommonTokenStream tokenStream = new CommonTokenStream(lexer); + ReportParserParser parser = new ReportParserParser(tokenStream); + ScriptErrorListener errorListener = new ScriptErrorListener(); + parser.removeErrorListeners(); + parser.addErrorListener(errorListener); + parser.expression(); + List infos = errorListener.getInfos(); + writeObjectToJson(resp, infos); + } + + public void conditionScriptValidation(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + String content = req.getParameter("content"); + ANTLRInputStream antlrInputStream = new ANTLRInputStream(content); + ReportParserLexer lexer = new ReportParserLexer(antlrInputStream); + CommonTokenStream tokenStream = new CommonTokenStream(lexer); + ReportParserParser parser = new ReportParserParser(tokenStream); + ScriptErrorListener errorListener = new ScriptErrorListener(); + parser.removeErrorListeners(); + parser.addErrorListener(errorListener); + parser.expr(); + List infos = errorListener.getInfos(); + writeObjectToJson(resp, infos); + } + + + public void parseDatasetName(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + String expr = req.getParameter("expr"); + ANTLRInputStream antlrInputStream = new ANTLRInputStream(expr); + ReportParserLexer lexer = new ReportParserLexer(antlrInputStream); + CommonTokenStream tokenStream = new CommonTokenStream(lexer); + ReportParserParser parser = new ReportParserParser(tokenStream); + parser.removeErrorListeners(); + DatasetContext ctx = parser.dataset(); + String datasetName = ctx.Identifier().getText(); + Map result = new HashMap(); + result.put("datasetName", datasetName); + writeObjectToJson(resp, result); + } + + public void savePreviewData(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + String content = req.getParameter("content"); + content = decodeContent(content); + InputStream inputStream = IOUtils.toInputStream(content, "utf-8"); + ReportDefinition reportDef = reportParser.parse(inputStream, "p"); + reportRender.rebuildReportDefinition(reportDef); + IOUtils.closeQuietly(inputStream); + TempObjectCache.putObject(PREVIEW_KEY, reportDef); + } + + public void loadReport(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + String file = req.getParameter("file"); + if (file == null) { + throw new ReportDesignException("Report file can not be null."); + } + file = ReportUtils.decodeFileName(file); + Object obj = TempObjectCache.getObject(file); + if (obj != null && obj instanceof ReportDefinition) { + ReportDefinition reportDef = (ReportDefinition) obj; + TempObjectCache.removeObject(file); + writeObjectToJson(resp, new ReportDefinitionWrapper(reportDef)); + } else { + ReportDefinition reportDef = reportRender.parseReport(file); + writeObjectToJson(resp, new ReportDefinitionWrapper(reportDef)); + } + } + + public void deleteReportFile(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + String file = req.getParameter("file"); + if (file == null) { + throw new ReportDesignException("Report file can not be null."); + } + ReportProvider targetReportProvider = null; + for (ReportProvider provider : reportProviders) { + if (file.startsWith(provider.getPrefix())) { + targetReportProvider = provider; + break; + } + } + if (targetReportProvider == null) { + throw new ReportDesignException("File [" + file + "] not found available report provider."); + } + targetReportProvider.deleteReport(file); + } + + + public void saveReportFile(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + String file = req.getParameter("file"); + file = ReportUtils.decodeFileName(file); + String content = req.getParameter("content"); + content = decodeContent(content); + ReportProvider targetReportProvider = null; + for (ReportProvider provider : reportProviders) { + if (file.startsWith(provider.getPrefix())) { + targetReportProvider = provider; + break; + } + } + if (targetReportProvider == null) { + throw new ReportDesignException("File [" + file + "] not found available report provider."); + } + targetReportProvider.saveReport(file, content); + InputStream inputStream = IOUtils.toInputStream(content, "utf-8"); + ReportDefinition reportDef = reportParser.parse(inputStream, file); + reportRender.rebuildReportDefinition(reportDef); + CacheUtils.cacheReportDefinition(file, reportDef); + IOUtils.closeQuietly(inputStream); + } + + public void loadReportProviders(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + writeObjectToJson(resp, reportProviders); + } + + public void setReportRender(ReportRender reportRender) { + this.reportRender = reportRender; + } + + public void setReportParser(ReportParser reportParser) { + this.reportParser = reportParser; + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + super.setApplicationContext(applicationContext); + Collection providers = applicationContext.getBeansOfType(ReportProvider.class).values(); + for (ReportProvider provider : providers) { + if (provider.disabled() || provider.getName() == null) { + continue; + } + reportProviders.add(provider); + } + } + + @Override + public String url() { + return "/designer"; + } +} diff --git a/blade-starter-report/src/main/java/com/bstek/ureport/console/designer/SearchFormDesignerAction.java b/blade-starter-report/src/main/java/com/bstek/ureport/console/designer/SearchFormDesignerAction.java new file mode 100644 index 0000000..be9d228 --- /dev/null +++ b/blade-starter-report/src/main/java/com/bstek/ureport/console/designer/SearchFormDesignerAction.java @@ -0,0 +1,50 @@ +/******************************************************************************* + * Copyright 2017 Bstek + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + ******************************************************************************/ +package com.bstek.ureport.console.designer; + +import com.bstek.ureport.console.RenderPageServletAction; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.apache.velocity.Template; +import org.apache.velocity.VelocityContext; + +import java.io.IOException; +import java.io.PrintWriter; + +/** + * @author Jacky.gao + * @since 2017年10月24日 + */ +public class SearchFormDesignerAction extends RenderPageServletAction { + + @Override + public void execute(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + VelocityContext context = new VelocityContext(); + context.put("contextPath", req.getContextPath()); + resp.setContentType("text/html"); + resp.setCharacterEncoding("utf-8"); + Template template = ve.getTemplate("ureport-html/searchform.html", "utf-8"); + PrintWriter writer = resp.getWriter(); + template.merge(context, writer); + writer.close(); + } + + @Override + public String url() { + return "/searchFormDesigner"; + } +} diff --git a/blade-starter-report/src/main/java/com/bstek/ureport/console/excel/ExportExcel97ServletAction.java b/blade-starter-report/src/main/java/com/bstek/ureport/console/excel/ExportExcel97ServletAction.java new file mode 100644 index 0000000..be5b76e --- /dev/null +++ b/blade-starter-report/src/main/java/com/bstek/ureport/console/excel/ExportExcel97ServletAction.java @@ -0,0 +1,119 @@ +/******************************************************************************* + * Copyright 2017 Bstek + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + ******************************************************************************/ +package com.bstek.ureport.console.excel; + +import com.bstek.ureport.build.ReportBuilder; +import com.bstek.ureport.console.BaseServletAction; +import com.bstek.ureport.console.cache.TempObjectCache; +import com.bstek.ureport.console.exception.ReportDesignException; +import com.bstek.ureport.definition.ReportDefinition; +import com.bstek.ureport.exception.ReportComputeException; +import com.bstek.ureport.export.ExportConfigure; +import com.bstek.ureport.export.ExportConfigureImpl; +import com.bstek.ureport.export.ExportManager; +import com.bstek.ureport.export.excel.low.Excel97Producer; +import com.bstek.ureport.model.Report; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.apache.commons.lang.StringUtils; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.Map; + +/** + * @author Jacky.gao + * @since 2017年7月3日 + */ +public class ExportExcel97ServletAction extends BaseServletAction { + private ReportBuilder reportBuilder; + private ExportManager exportManager; + private Excel97Producer excelProducer = new Excel97Producer(); + + @Override + public void execute(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + String method = retriveMethod(req); + if (method != null) { + invokeMethod(method, req, resp); + } else { + buildExcel(req, resp, false, false); + } + } + + public void paging(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + buildExcel(req, resp, true, false); + } + + public void sheet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + buildExcel(req, resp, false, true); + } + + public void buildExcel(HttpServletRequest req, HttpServletResponse resp, boolean withPage, boolean withSheet) throws IOException { + String file = req.getParameter("_u"); + if (StringUtils.isBlank(file)) { + throw new ReportComputeException("Report file can not be null."); + } + String fileName = req.getParameter("_n"); + if (StringUtils.isNotBlank(fileName)) { + fileName = decode(fileName); + } else { + fileName = "ureport.xls"; + } + resp.setContentType("application/octet-stream;charset=ISO8859-1"); + resp.setHeader("Content-Disposition", "attachment;filename=\"" + fileName + "\""); + Map parameters = buildParameters(req); + OutputStream outputStream = resp.getOutputStream(); + if (file.equals(PREVIEW_KEY)) { + ReportDefinition reportDefinition = (ReportDefinition) TempObjectCache.getObject(PREVIEW_KEY); + if (reportDefinition == null) { + throw new ReportDesignException("Report data has expired,can not do export excel."); + } + Report report = reportBuilder.buildReport(reportDefinition, parameters); + if (withPage) { + excelProducer.produceWithPaging(report, outputStream); + } else if (withSheet) { + excelProducer.produceWithSheet(report, outputStream); + } else { + excelProducer.produce(report, outputStream); + } + } else { + ExportConfigure configure = new ExportConfigureImpl(file, parameters, outputStream); + if (withPage) { + exportManager.exportExcelWithPaging(configure); + } else if (withSheet) { + exportManager.exportExcelWithPagingSheet(configure); + } else { + exportManager.exportExcel(configure); + } + } + outputStream.flush(); + outputStream.close(); + } + + public void setReportBuilder(ReportBuilder reportBuilder) { + this.reportBuilder = reportBuilder; + } + + public void setExportManager(ExportManager exportManager) { + this.exportManager = exportManager; + } + + @Override + public String url() { + return "/excel97"; + } +} diff --git a/blade-starter-report/src/main/java/com/bstek/ureport/console/excel/ExportExcelServletAction.java b/blade-starter-report/src/main/java/com/bstek/ureport/console/excel/ExportExcelServletAction.java new file mode 100644 index 0000000..772b8b4 --- /dev/null +++ b/blade-starter-report/src/main/java/com/bstek/ureport/console/excel/ExportExcelServletAction.java @@ -0,0 +1,123 @@ +/******************************************************************************* + * Copyright 2017 Bstek + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + ******************************************************************************/ +package com.bstek.ureport.console.excel; + +import com.bstek.ureport.build.ReportBuilder; +import com.bstek.ureport.console.BaseServletAction; +import com.bstek.ureport.console.cache.TempObjectCache; +import com.bstek.ureport.console.exception.ReportDesignException; +import com.bstek.ureport.definition.ReportDefinition; +import com.bstek.ureport.exception.ReportComputeException; +import com.bstek.ureport.exception.ReportException; +import com.bstek.ureport.export.ExportConfigure; +import com.bstek.ureport.export.ExportConfigureImpl; +import com.bstek.ureport.export.ExportManager; +import com.bstek.ureport.export.excel.high.ExcelProducer; +import com.bstek.ureport.model.Report; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.apache.commons.lang.StringUtils; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.Map; + +/** + * @author Jacky.gao + * @since 2017年4月17日 + */ +public class ExportExcelServletAction extends BaseServletAction { + private ReportBuilder reportBuilder; + private ExportManager exportManager; + private ExcelProducer excelProducer = new ExcelProducer(); + + @Override + public void execute(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + String method = retriveMethod(req); + if (method != null) { + invokeMethod(method, req, resp); + } else { + buildExcel(req, resp, false, false); + } + } + + public void paging(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + buildExcel(req, resp, true, false); + } + + public void sheet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + buildExcel(req, resp, false, true); + } + + public void buildExcel(HttpServletRequest req, HttpServletResponse resp, boolean withPage, boolean withSheet) throws IOException { + String file = req.getParameter("_u"); + file = decode(file); + if (StringUtils.isBlank(file)) { + throw new ReportComputeException("Report file can not be null."); + } + OutputStream outputStream = resp.getOutputStream(); + try { + String fileName = req.getParameter("_n"); + fileName = buildDownloadFileName(file, fileName, ".xlsx"); + resp.setContentType("application/octet-stream;charset=ISO8859-1"); + fileName = new String(fileName.getBytes("UTF-8"), "ISO8859-1"); + resp.setHeader("Content-Disposition", "attachment;filename=\"" + fileName + "\""); + Map parameters = buildParameters(req); + if (file.equals(PREVIEW_KEY)) { + ReportDefinition reportDefinition = (ReportDefinition) TempObjectCache.getObject(PREVIEW_KEY); + if (reportDefinition == null) { + throw new ReportDesignException("Report data has expired,can not do export excel."); + } + Report report = reportBuilder.buildReport(reportDefinition, parameters); + if (withPage) { + excelProducer.produceWithPaging(report, outputStream); + } else if (withSheet) { + excelProducer.produceWithSheet(report, outputStream); + } else { + excelProducer.produce(report, outputStream); + } + } else { + ExportConfigure configure = new ExportConfigureImpl(file, parameters, outputStream); + if (withPage) { + exportManager.exportExcelWithPaging(configure); + } else if (withSheet) { + exportManager.exportExcelWithPagingSheet(configure); + } else { + exportManager.exportExcel(configure); + } + } + } catch (Exception ex) { + throw new ReportException(ex); + } finally { + outputStream.flush(); + outputStream.close(); + } + } + + public void setReportBuilder(ReportBuilder reportBuilder) { + this.reportBuilder = reportBuilder; + } + + public void setExportManager(ExportManager exportManager) { + this.exportManager = exportManager; + } + + @Override + public String url() { + return "/excel"; + } +} diff --git a/blade-starter-report/src/main/java/com/bstek/ureport/console/html/HtmlPreviewServletAction.java b/blade-starter-report/src/main/java/com/bstek/ureport/console/html/HtmlPreviewServletAction.java new file mode 100644 index 0000000..85be661 --- /dev/null +++ b/blade-starter-report/src/main/java/com/bstek/ureport/console/html/HtmlPreviewServletAction.java @@ -0,0 +1,365 @@ +/******************************************************************************* + * Copyright 2017 Bstek + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + ******************************************************************************/ +package com.bstek.ureport.console.html; + +import com.bstek.ureport.build.Context; +import com.bstek.ureport.build.ReportBuilder; +import com.bstek.ureport.build.paging.Page; +import com.bstek.ureport.cache.CacheUtils; +import com.bstek.ureport.chart.ChartData; +import com.bstek.ureport.console.MobileUtils; +import com.bstek.ureport.console.RenderPageServletAction; +import com.bstek.ureport.console.cache.TempObjectCache; +import com.bstek.ureport.console.exception.ReportDesignException; +import com.bstek.ureport.definition.Paper; +import com.bstek.ureport.definition.ReportDefinition; +import com.bstek.ureport.definition.searchform.FormPosition; +import com.bstek.ureport.exception.ReportComputeException; +import com.bstek.ureport.export.*; +import com.bstek.ureport.export.html.HtmlProducer; +import com.bstek.ureport.export.html.HtmlReport; +import com.bstek.ureport.export.html.SearchFormData; +import com.bstek.ureport.model.Report; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.apache.commons.lang.StringUtils; +import org.apache.velocity.Template; +import org.apache.velocity.VelocityContext; +import org.codehaus.jackson.map.ObjectMapper; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.*; + +/** + * @author Jacky.gao + * @since 2017年2月15日 + */ +public class HtmlPreviewServletAction extends RenderPageServletAction { + private ExportManager exportManager; + private ReportBuilder reportBuilder; + private ReportRender reportRender; + private HtmlProducer htmlProducer = new HtmlProducer(); + + @Override + public void execute(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + String method = retriveMethod(req); + if (method != null) { + invokeMethod(method, req, resp); + } else { + VelocityContext context = new VelocityContext(); + HtmlReport htmlReport = null; + String errorMsg = null; + try { + htmlReport = loadReport(req); + } catch (Exception ex) { + if (!(ex instanceof ReportDesignException)) { + ex.printStackTrace(); + } + errorMsg = buildExceptionMessage(ex); + } + String title = buildTitle(req); + context.put("title", title); + if (htmlReport == null) { + context.put("content", "
报表计算出错,错误信息如下:
" + errorMsg + "
"); + context.put("error", true); + context.put("searchFormJs", ""); + context.put("downSearchFormHtml", ""); + context.put("upSearchFormHtml", ""); + } else { + SearchFormData formData = htmlReport.getSearchFormData(); + if (formData != null) { + context.put("searchFormJs", formData.getJs()); + if (formData.getFormPosition().equals(FormPosition.up)) { + context.put("upSearchFormHtml", formData.getHtml()); + context.put("downSearchFormHtml", ""); + } else { + context.put("downSearchFormHtml", formData.getHtml()); + context.put("upSearchFormHtml", ""); + } + } else { + context.put("searchFormJs", ""); + context.put("downSearchFormHtml", ""); + context.put("upSearchFormHtml", ""); + } + context.put("content", htmlReport.getContent()); + context.put("style", htmlReport.getStyle()); + context.put("reportAlign", htmlReport.getReportAlign()); + context.put("totalPage", htmlReport.getTotalPage()); + context.put("totalPageWithCol", htmlReport.getTotalPageWithCol()); + context.put("pageIndex", htmlReport.getPageIndex()); + context.put("chartDatas", convertJson(htmlReport.getChartDatas())); + context.put("error", false); + context.put("file", req.getParameter("_u")); + context.put("intervalRefreshValue", htmlReport.getHtmlIntervalRefreshValue()); + String customParameters = buildCustomParameters(req); + context.put("customParameters", customParameters); + context.put("_t", ""); + Tools tools = null; + if (MobileUtils.isMobile(req)) { + tools = new Tools(false); + tools.setShow(false); + } else { + String toolsInfo = req.getParameter("_t"); + if (StringUtils.isNotBlank(toolsInfo)) { + tools = new Tools(false); + if (toolsInfo.equals("0")) { + tools.setShow(false); + } else { + String[] infos = toolsInfo.split(","); + for (String name : infos) { + tools.doInit(name); + } + } + context.put("_t", toolsInfo); + context.put("hasTools", true); + } else { + tools = new Tools(true); + } + } + context.put("tools", tools); + } + context.put("contextPath", req.getContextPath()); + resp.setContentType("text/html"); + resp.setCharacterEncoding("utf-8"); + Template template = ve.getTemplate("ureport-html/html-preview.html", "utf-8"); + PrintWriter writer = resp.getWriter(); + template.merge(context, writer); + writer.close(); + } + } + + private String buildTitle(HttpServletRequest req) { + String title = req.getParameter("_title"); + if (StringUtils.isBlank(title)) { + title = req.getParameter("_u"); + title = decode(title); + int point = title.lastIndexOf(".ureport.xml"); + if (point > -1) { + title = title.substring(0, point); + } + if (title.equals("p")) { + title = "设计中报表"; + } + } else { + title = decode(title); + } + return title + "-ureport"; + } + + private String convertJson(Collection data) { + if (data == null || data.size() == 0) { + return ""; + } + ObjectMapper mapper = new ObjectMapper(); + try { + String json = mapper.writeValueAsString(data); + return json; + } catch (Exception e) { + throw new ReportComputeException(e); + } + } + + public void loadData(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + HtmlReport htmlReport = loadReport(req); + writeObjectToJson(resp, htmlReport); + } + + public void loadPrintPages(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + String file = req.getParameter("_u"); + file = decode(file); + if (StringUtils.isBlank(file)) { + throw new ReportComputeException("Report file can not be null."); + } + Map parameters = buildParameters(req); + ReportDefinition reportDefinition = null; + if (file.equals(PREVIEW_KEY)) { + reportDefinition = (ReportDefinition) TempObjectCache.getObject(PREVIEW_KEY); + if (reportDefinition == null) { + throw new ReportDesignException("Report data has expired,can not do export excel."); + } + } else { + reportDefinition = reportRender.getReportDefinition(file); + } + Report report = reportBuilder.buildReport(reportDefinition, parameters); + Map chartMap = report.getContext().getChartDataMap(); + if (chartMap.size() > 0) { + CacheUtils.storeChartDataMap(chartMap); + } + FullPageData pageData = PageBuilder.buildFullPageData(report); + StringBuilder sb = new StringBuilder(); + List> list = pageData.getPageList(); + Context context = report.getContext(); + if (list.size() > 0) { + for (int i = 0; i < list.size(); i++) { + List columnPages = list.get(i); + if (i == 0) { + String html = htmlProducer.produce(context, columnPages, pageData.getColumnMargin(), false); + sb.append(html); + } else { + String html = htmlProducer.produce(context, columnPages, pageData.getColumnMargin(), false); + sb.append(html); + } + } + } else { + List pages = report.getPages(); + for (int i = 0; i < pages.size(); i++) { + Page page = pages.get(i); + if (i == 0) { + String html = htmlProducer.produce(context, page, false); + sb.append(html); + } else { + String html = htmlProducer.produce(context, page, true); + sb.append(html); + } + } + } + Map map = new HashMap(); + map.put("html", sb.toString()); + writeObjectToJson(resp, map); + } + + public void loadPagePaper(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + String file = req.getParameter("_u"); + file = decode(file); + if (StringUtils.isBlank(file)) { + throw new ReportComputeException("Report file can not be null."); + } + ReportDefinition report = null; + if (file.equals(PREVIEW_KEY)) { + report = (ReportDefinition) TempObjectCache.getObject(PREVIEW_KEY); + if (report == null) { + throw new ReportDesignException("Report data has expired."); + } + } else { + report = reportRender.getReportDefinition(file); + } + Paper paper = report.getPaper(); + writeObjectToJson(resp, paper); + } + + private HtmlReport loadReport(HttpServletRequest req) { + Map parameters = buildParameters(req); + HtmlReport htmlReport = null; + String file = req.getParameter("_u"); + file = decode(file); + String pageIndex = req.getParameter("_i"); + if (StringUtils.isBlank(file)) { + throw new ReportComputeException("Report file can not be null."); + } + if (file.equals(PREVIEW_KEY)) { + ReportDefinition reportDefinition = (ReportDefinition) TempObjectCache.getObject(PREVIEW_KEY); + if (reportDefinition == null) { + throw new ReportDesignException("Report data has expired,can not do preview."); + } + Report report = reportBuilder.buildReport(reportDefinition, parameters); + Map chartMap = report.getContext().getChartDataMap(); + if (chartMap.size() > 0) { + CacheUtils.storeChartDataMap(chartMap); + } + htmlReport = new HtmlReport(); + String html = null; + if (StringUtils.isNotBlank(pageIndex) && !pageIndex.equals("0")) { + Context context = report.getContext(); + int index = Integer.valueOf(pageIndex); + SinglePageData pageData = PageBuilder.buildSinglePageData(index, report); + List pages = pageData.getPages(); + if (pages.size() == 1) { + Page page = pages.get(0); + html = htmlProducer.produce(context, page, false); + } else { + html = htmlProducer.produce(context, pages, pageData.getColumnMargin(), false); + } + htmlReport.setTotalPage(pageData.getTotalPages()); + htmlReport.setPageIndex(index); + } else { + html = htmlProducer.produce(report); + } + if (report.getPaper().isColumnEnabled()) { + htmlReport.setColumn(report.getPaper().getColumnCount()); + } + htmlReport.setChartDatas(report.getContext().getChartDataMap().values()); + htmlReport.setContent(html); + htmlReport.setTotalPage(report.getPages().size()); + htmlReport.setStyle(reportDefinition.getStyle()); + htmlReport.setSearchFormData(reportDefinition.buildSearchFormData(report.getContext().getDatasetMap(), parameters)); + htmlReport.setReportAlign(report.getPaper().getHtmlReportAlign().name()); + htmlReport.setHtmlIntervalRefreshValue(report.getPaper().getHtmlIntervalRefreshValue()); + } else { + if (StringUtils.isNotBlank(pageIndex) && !pageIndex.equals("0")) { + int index = Integer.valueOf(pageIndex); + htmlReport = exportManager.exportHtml(file, req.getContextPath(), parameters, index); + } else { + htmlReport = exportManager.exportHtml(file, req.getContextPath(), parameters); + } + } + return htmlReport; + } + + + private String buildCustomParameters(HttpServletRequest req) { + StringBuilder sb = new StringBuilder(); + Enumeration enumeration = req.getParameterNames(); + while (enumeration.hasMoreElements()) { + Object obj = enumeration.nextElement(); + if (obj == null) { + continue; + } + String name = obj.toString(); + String value = req.getParameter(name); + if (name == null || value == null || (name.startsWith("_") && !name.equals("_n"))) { + continue; + } + if (sb.length() > 0) { + sb.append("&"); + } + sb.append(name); + sb.append("="); + sb.append(value); + } + return sb.toString(); + } + + private String buildExceptionMessage(Throwable throwable) { + Throwable root = buildRootException(throwable); + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + root.printStackTrace(pw); + String trace = sw.getBuffer().toString(); + trace = trace.replaceAll("\n", "
"); + pw.close(); + return trace; + } + + public void setExportManager(ExportManager exportManager) { + this.exportManager = exportManager; + } + + public void setReportBuilder(ReportBuilder reportBuilder) { + this.reportBuilder = reportBuilder; + } + + public void setReportRender(ReportRender reportRender) { + this.reportRender = reportRender; + } + + @Override + public String url() { + return "/preview"; + } +} diff --git a/blade-starter-report/src/main/java/com/bstek/ureport/console/image/ImageServletAction.java b/blade-starter-report/src/main/java/com/bstek/ureport/console/image/ImageServletAction.java new file mode 100644 index 0000000..25e7efd --- /dev/null +++ b/blade-starter-report/src/main/java/com/bstek/ureport/console/image/ImageServletAction.java @@ -0,0 +1,61 @@ +/******************************************************************************* + * Copyright 2017 Bstek + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + ******************************************************************************/ +package com.bstek.ureport.console.image; + +import com.bstek.ureport.cache.ResourceCache; +import com.bstek.ureport.console.ServletAction; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.StringUtils; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * @author Jacky.gao + * @since 2016年6月6日 + */ +public class ImageServletAction implements ServletAction { + public static final String URL = "/image"; + + @Override + public void execute(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + String key = req.getParameter("_key"); + if (StringUtils.isNotBlank(key)) { + byte[] bytes = (byte[]) ResourceCache.getObject(key); + InputStream input = new ByteArrayInputStream(bytes); + OutputStream output = resp.getOutputStream(); + resp.setContentType("image/png"); + try { + IOUtils.copy(input, output); + } finally { + IOUtils.closeQuietly(input); + IOUtils.closeQuietly(output); + } + } else { + //processImage(req, resp); + } + } + + @Override + public String url() { + return URL; + } +} diff --git a/blade-starter-report/src/main/java/com/bstek/ureport/console/importexcel/ImportExcelServletAction.java b/blade-starter-report/src/main/java/com/bstek/ureport/console/importexcel/ImportExcelServletAction.java new file mode 100644 index 0000000..e1f4fa0 --- /dev/null +++ b/blade-starter-report/src/main/java/com/bstek/ureport/console/importexcel/ImportExcelServletAction.java @@ -0,0 +1,87 @@ +/******************************************************************************* + * Copyright 2017 Bstek + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + ******************************************************************************/ +package com.bstek.ureport.console.importexcel; + +import com.bstek.ureport.console.RenderPageServletAction; +import com.bstek.ureport.console.cache.TempObjectCache; +import com.bstek.ureport.definition.ReportDefinition; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springblade.core.tool.utils.MultipartUtil; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.io.InputStream; +import java.util.*; + +/** + * @author Jacky.gao + * @since 2017年5月25日 + */ +public class ImportExcelServletAction extends RenderPageServletAction { + private List excelParsers = new ArrayList(); + + public ImportExcelServletAction() { + excelParsers.add(new HSSFExcelParser()); + excelParsers.add(new XSSFExcelParser()); + } + + @Override + public void execute(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + String errorInfo; + ReportDefinition report = null; + try { + List items = MultipartUtil.extractMultipartFiles(req); + for (MultipartFile item : items) { + String fieldName = item.getName(); + String name = Objects.requireNonNull(item.getOriginalFilename()).toLowerCase(); + if (fieldName.equals("_excel_file") && (name.endsWith(".xls") || name.endsWith(".xlsx"))) { + InputStream inputStream = item.getInputStream(); + for (ExcelParser parser : excelParsers) { + if (parser.support(name)) { + report = parser.parse(inputStream); + break; + } + } + inputStream.close(); + break; + } + } + errorInfo = "请选择一个合法的Excel导入"; + } catch (Exception e) { + e.printStackTrace(); + errorInfo = e.getMessage(); + } + + Map result = new HashMap(); + if (report != null) { + result.put("result", true); + TempObjectCache.putObject("classpath:template/template.ureport.xml", report); + } else { + result.put("result", false); + if (errorInfo != null) { + result.put("errorInfo", errorInfo); + } + } + writeObjectToJson(resp, result); + } + + @Override + public String url() { + return "/import"; + } +} diff --git a/blade-starter-report/src/main/java/com/bstek/ureport/console/pdf/ExportPdfServletAction.java b/blade-starter-report/src/main/java/com/bstek/ureport/console/pdf/ExportPdfServletAction.java new file mode 100644 index 0000000..d52f477 --- /dev/null +++ b/blade-starter-report/src/main/java/com/bstek/ureport/console/pdf/ExportPdfServletAction.java @@ -0,0 +1,144 @@ +/******************************************************************************* + * Copyright 2017 Bstek + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + ******************************************************************************/ +package com.bstek.ureport.console.pdf; + +import com.bstek.ureport.build.ReportBuilder; +import com.bstek.ureport.console.BaseServletAction; +import com.bstek.ureport.console.cache.TempObjectCache; +import com.bstek.ureport.console.exception.ReportDesignException; +import com.bstek.ureport.definition.Paper; +import com.bstek.ureport.definition.ReportDefinition; +import com.bstek.ureport.exception.ReportComputeException; +import com.bstek.ureport.exception.ReportException; +import com.bstek.ureport.export.ExportConfigure; +import com.bstek.ureport.export.ExportConfigureImpl; +import com.bstek.ureport.export.ExportManager; +import com.bstek.ureport.export.ReportRender; +import com.bstek.ureport.export.pdf.PdfProducer; +import com.bstek.ureport.model.Report; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.apache.commons.lang.StringUtils; +import org.codehaus.jackson.map.ObjectMapper; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.Map; + +/** + * @author Jacky.gao + * @since 2017年3月20日 + */ +public class ExportPdfServletAction extends BaseServletAction { + private ReportBuilder reportBuilder; + private ExportManager exportManager; + private ReportRender reportRender; + private PdfProducer pdfProducer = new PdfProducer(); + + @Override + public void execute(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + String method = retriveMethod(req); + if (method != null) { + invokeMethod(method, req, resp); + } else { + buildPdf(req, resp, false); + } + } + + public void show(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + buildPdf(req, resp, true); + } + + public void buildPdf(HttpServletRequest req, HttpServletResponse resp, boolean forPrint) throws IOException { + String file = req.getParameter("_u"); + file = decode(file); + if (StringUtils.isBlank(file)) { + throw new ReportComputeException("Report file can not be null."); + } + OutputStream outputStream = null; + try { + String fileName = req.getParameter("_n"); + fileName = buildDownloadFileName(file, fileName, ".pdf"); + fileName = new String(fileName.getBytes("UTF-8"), "ISO8859-1"); + if (forPrint) { + resp.setContentType("application/pdf"); + resp.setHeader("Content-Disposition", "inline;filename=\"" + fileName + "\""); + } else { + resp.setContentType("application/octet-stream;charset=ISO8859-1"); + resp.setHeader("Content-Disposition", "attachment;filename=\"" + fileName + "\""); + } + outputStream = resp.getOutputStream(); + Map parameters = buildParameters(req); + if (file.equals(PREVIEW_KEY)) { + ReportDefinition reportDefinition = (ReportDefinition) TempObjectCache.getObject(PREVIEW_KEY); + if (reportDefinition == null) { + throw new ReportDesignException("Report data has expired,can not do export pdf."); + } + Report report = reportBuilder.buildReport(reportDefinition, parameters); + pdfProducer.produce(report, outputStream); + } else { + ExportConfigure configure = new ExportConfigureImpl(file, parameters, outputStream); + exportManager.exportPdf(configure); + } + } catch (Exception ex) { + throw new ReportException(ex); + } finally { + outputStream.flush(); + outputStream.close(); + } + } + + public void newPaging(HttpServletRequest req, HttpServletResponse resp) throws IOException { + String file = req.getParameter("_u"); + if (StringUtils.isBlank(file)) { + throw new ReportComputeException("Report file can not be null."); + } + Report report = null; + Map parameters = buildParameters(req); + if (file.equals(PREVIEW_KEY)) { + ReportDefinition reportDefinition = (ReportDefinition) TempObjectCache.getObject(PREVIEW_KEY); + if (reportDefinition == null) { + throw new ReportDesignException("Report data has expired,can not do export pdf."); + } + report = reportBuilder.buildReport(reportDefinition, parameters); + } else { + ReportDefinition reportDefinition = reportRender.getReportDefinition(file); + report = reportRender.render(reportDefinition, parameters); + } + String paper = req.getParameter("_paper"); + ObjectMapper mapper = new ObjectMapper(); + Paper newPaper = mapper.readValue(paper, Paper.class); + report.rePaging(newPaper); + } + + public void setReportRender(ReportRender reportRender) { + this.reportRender = reportRender; + } + + public void setExportManager(ExportManager exportManager) { + this.exportManager = exportManager; + } + + public void setReportBuilder(ReportBuilder reportBuilder) { + this.reportBuilder = reportBuilder; + } + + @Override + public String url() { + return "/pdf"; + } +} diff --git a/blade-starter-report/src/main/java/com/bstek/ureport/console/res/ResourceLoaderServletAction.java b/blade-starter-report/src/main/java/com/bstek/ureport/console/res/ResourceLoaderServletAction.java new file mode 100644 index 0000000..c5f0324 --- /dev/null +++ b/blade-starter-report/src/main/java/com/bstek/ureport/console/res/ResourceLoaderServletAction.java @@ -0,0 +1,78 @@ +/******************************************************************************* + * Copyright 2017 Bstek + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + ******************************************************************************/ +package com.bstek.ureport.console.res; + +import com.bstek.ureport.console.ServletAction; +import com.bstek.ureport.console.UReportServlet; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.apache.commons.io.IOUtils; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * @author Jacky.gao + * @since 2016年6月6日 + */ +public class ResourceLoaderServletAction implements ServletAction, ApplicationContextAware { + public static final String URL = "/res"; + private ApplicationContext applicationContext; + + @Override + public void execute(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + String path = req.getContextPath() + UReportServlet.URL + URL; + String uri = req.getRequestURI(); + String resPath = uri.substring(path.length() + 1); + String p = "classpath:" + resPath; + if (p.endsWith(".js")) { + resp.setContentType("text/javascript"); + } else if (p.endsWith(".css")) { + resp.setContentType("text/css"); + } else if (p.endsWith(".png")) { + resp.setContentType("image/png"); + } else if (p.endsWith(".jpg")) { + resp.setContentType("image/jpeg"); + } else if (p.endsWith(".svg")) { + resp.setContentType("image/svg+xml"); + } else { + resp.setContentType("application/octet-stream"); + } + InputStream input = applicationContext.getResource(p).getInputStream(); + OutputStream output = resp.getOutputStream(); + try { + IOUtils.copy(input, output); + } finally { + IOUtils.closeQuietly(input); + IOUtils.closeQuietly(output); + } + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + + @Override + public String url() { + return URL; + } +} diff --git a/blade-starter-report/src/main/java/com/bstek/ureport/console/word/ExportWordServletAction.java b/blade-starter-report/src/main/java/com/bstek/ureport/console/word/ExportWordServletAction.java new file mode 100644 index 0000000..e4ec473 --- /dev/null +++ b/blade-starter-report/src/main/java/com/bstek/ureport/console/word/ExportWordServletAction.java @@ -0,0 +1,103 @@ +/******************************************************************************* + * Copyright 2017 Bstek + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + ******************************************************************************/ +package com.bstek.ureport.console.word; + +import com.bstek.ureport.build.ReportBuilder; +import com.bstek.ureport.console.BaseServletAction; +import com.bstek.ureport.console.cache.TempObjectCache; +import com.bstek.ureport.console.exception.ReportDesignException; +import com.bstek.ureport.definition.ReportDefinition; +import com.bstek.ureport.exception.ReportComputeException; +import com.bstek.ureport.exception.ReportException; +import com.bstek.ureport.export.ExportConfigure; +import com.bstek.ureport.export.ExportConfigureImpl; +import com.bstek.ureport.export.ExportManager; +import com.bstek.ureport.export.word.high.WordProducer; +import com.bstek.ureport.model.Report; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.apache.commons.lang.StringUtils; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.Map; + +/** + * @author Jacky.gao + * @since 2017年4月17日 + */ +public class ExportWordServletAction extends BaseServletAction { + private ReportBuilder reportBuilder; + private ExportManager exportManager; + private WordProducer wordProducer = new WordProducer(); + + @Override + public void execute(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + String method = retriveMethod(req); + if (method != null) { + invokeMethod(method, req, resp); + } else { + buildWord(req, resp); + } + } + + public void buildWord(HttpServletRequest req, HttpServletResponse resp) throws IOException { + String file = req.getParameter("_u"); + file = decode(file); + if (StringUtils.isBlank(file)) { + throw new ReportComputeException("Report file can not be null."); + } + OutputStream outputStream = resp.getOutputStream(); + try { + String fileName = req.getParameter("_n"); + fileName = buildDownloadFileName(file, fileName, ".docx"); + fileName = new String(fileName.getBytes("UTF-8"), "ISO8859-1"); + resp.setContentType("application/octet-stream;charset=ISO8859-1"); + resp.setHeader("Content-Disposition", "attachment;filename=\"" + fileName + "\""); + Map parameters = buildParameters(req); + if (file.equals(PREVIEW_KEY)) { + ReportDefinition reportDefinition = (ReportDefinition) TempObjectCache.getObject(PREVIEW_KEY); + if (reportDefinition == null) { + throw new ReportDesignException("Report data has expired,can not do export word."); + } + Report report = reportBuilder.buildReport(reportDefinition, parameters); + wordProducer.produce(report, outputStream); + } else { + ExportConfigure configure = new ExportConfigureImpl(file, parameters, outputStream); + exportManager.exportWord(configure); + } + } catch (Exception ex) { + throw new ReportException(ex); + } finally { + outputStream.flush(); + outputStream.close(); + } + } + + public void setReportBuilder(ReportBuilder reportBuilder) { + this.reportBuilder = reportBuilder; + } + + public void setExportManager(ExportManager exportManager) { + this.exportManager = exportManager; + } + + @Override + public String url() { + return "/word"; + } +} diff --git a/blade-starter-report/src/main/java/com/bstek/ureport/definition/CellStyle.java b/blade-starter-report/src/main/java/com/bstek/ureport/definition/CellStyle.java new file mode 100644 index 0000000..b2440b2 --- /dev/null +++ b/blade-starter-report/src/main/java/com/bstek/ureport/definition/CellStyle.java @@ -0,0 +1,202 @@ +/******************************************************************************* + * Copyright 2017 Bstek + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + ******************************************************************************/ +package com.bstek.ureport.definition; + +import com.bstek.ureport.export.pdf.font.FontBuilder; +import org.apache.commons.lang.StringUtils; +import org.codehaus.jackson.annotate.JsonIgnore; + +import java.awt.*; +import java.io.Serializable; + + +/** + * @author Jacky.gao + * @since 2017年1月18日 + */ +public class CellStyle implements Serializable { + private static final long serialVersionUID = 8327688051735343849L; + private String bgcolor; + private String forecolor; + private int fontSize; + private String fontFamily; + private String format; + private float lineHeight; + private Alignment align; + private Alignment valign; + private Boolean bold; + private Boolean italic; + private Boolean underline; + private Boolean wrapCompute; + private Border leftBorder; + private Border rightBorder; + private Border topBorder; + private Border bottomBorder; + + private Font font; + + public Border getLeftBorder() { + return leftBorder; + } + + public void setLeftBorder(Border leftBorder) { + this.leftBorder = leftBorder; + } + + public Border getRightBorder() { + return rightBorder; + } + + public void setRightBorder(Border rightBorder) { + this.rightBorder = rightBorder; + } + + public Border getTopBorder() { + return topBorder; + } + + public void setTopBorder(Border topBorder) { + this.topBorder = topBorder; + } + + public Border getBottomBorder() { + return bottomBorder; + } + + public void setBottomBorder(Border bottomBorder) { + this.bottomBorder = bottomBorder; + } + + public String getBgcolor() { + return bgcolor; + } + + public void setBgcolor(String bgcolor) { + this.bgcolor = bgcolor; + } + + public String getForecolor() { + return forecolor; + } + + public void setForecolor(String forecolor) { + this.forecolor = forecolor; + } + + public int getFontSize() { + return fontSize; + } + + public void setFontSize(int fontSize) { + this.fontSize = fontSize; + } + + public String getFontFamily() { + return fontFamily; + } + + public void setFontFamily(String fontFamily) { + this.fontFamily = fontFamily; + } + + public String getFormat() { + return format; + } + + public void setFormat(String format) { + this.format = format; + } + + public Alignment getAlign() { + return align; + } + + public void setAlign(Alignment align) { + this.align = align; + } + + public Alignment getValign() { + return valign; + } + + public void setValign(Alignment valign) { + this.valign = valign; + } + + public Boolean getBold() { + return bold; + } + + public void setBold(Boolean bold) { + this.bold = bold; + } + + public Boolean getItalic() { + return italic; + } + + public void setItalic(Boolean italic) { + this.italic = italic; + } + + public Boolean getUnderline() { + return underline; + } + + public void setUnderline(Boolean underline) { + this.underline = underline; + } + + public Boolean getWrapCompute() { + return wrapCompute; + } + + public void setWrapCompute(Boolean wrapCompute) { + this.wrapCompute = wrapCompute; + } + + public void setFont(Font font) { + this.font = font; + } + + public float getLineHeight() { + return lineHeight; + } + + public void setLineHeight(float lineHeight) { + this.lineHeight = lineHeight; + } + + @JsonIgnore + public Font getFont() { + if (this.font == null) { + int fontStyle = Font.PLAIN; + if ((bold != null && bold) && (italic != null && italic)) { + fontStyle = Font.BOLD | Font.ITALIC; + } else if (bold != null && bold) { + fontStyle = Font.BOLD; + } else if (italic != null && italic) { + fontStyle = Font.ITALIC; + } + String fontName = fontFamily; + if (StringUtils.isBlank(fontName)) { + fontName = "宋体"; + } + this.font = FontBuilder.getAwtFont(fontName, fontStyle, (float) fontSize); + } + return this.font; + } +} diff --git a/blade-starter-report/src/main/java/com/bstek/ureport/export/pdf/font/FontBuilder.java b/blade-starter-report/src/main/java/com/bstek/ureport/export/pdf/font/FontBuilder.java new file mode 100644 index 0000000..4208ac8 --- /dev/null +++ b/blade-starter-report/src/main/java/com/bstek/ureport/export/pdf/font/FontBuilder.java @@ -0,0 +1,156 @@ +/******************************************************************************* + * Copyright 2017 Bstek + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + ******************************************************************************/ +package com.bstek.ureport.export.pdf.font; + +import com.bstek.ureport.exception.ReportComputeException; +import com.bstek.ureport.exception.ReportException; +import com.itextpdf.text.DocumentException; +import com.itextpdf.text.Font; +import com.itextpdf.text.FontFactory; +import com.itextpdf.text.pdf.BaseFont; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; + +import java.awt.*; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.*; + +/** + * @author Jacky.gao + * @since 2014年4月22日 + */ +public class FontBuilder implements ApplicationContextAware { + private static ApplicationContext applicationContext; + private static final Map fontMap = new HashMap(); + public static final Map fontPathMap = new HashMap(); + private static List systemFontNameList = new ArrayList(); + + public static Font getFont(String fontName, int fontSize, boolean fontBold, boolean fontItalic, boolean underLine) { + BaseFont baseFont = fontMap.get(fontName); + Font font = null; + if (baseFont != null) { + font = new Font(baseFont); + } else { + font = FontFactory.getFont(fontName); + } + font.setSize(fontSize); + int fontStyle = Font.NORMAL; + if (fontBold && fontItalic && underLine) { + fontStyle = Font.BOLD | Font.ITALIC | Font.UNDERLINE; + } else if (fontBold) { + if (fontItalic) { + fontStyle = Font.BOLD | Font.ITALIC; + } else if (underLine) { + fontStyle = Font.BOLD | Font.UNDERLINE; + } else { + fontStyle = Font.BOLD; + } + } else if (fontItalic) { + if (underLine) { + fontStyle = Font.ITALIC | Font.UNDERLINE; + } else if (fontBold) { + fontStyle = Font.ITALIC | Font.BOLD; + } else { + fontStyle = Font.ITALIC; + } + } else if (underLine) { + fontStyle = Font.UNDERLINE; + } + font.setStyle(fontStyle); + return font; + } + + public static java.awt.Font getAwtFont(String fontName, int fontStyle, float size) { + if (systemFontNameList.contains(fontName)) { + return new java.awt.Font(fontName, fontStyle, Integer.parseInt(String.valueOf(size))); + } + String fontPath = fontPathMap.get(fontName); + if (fontPath == null) { + fontName = "宋体"; + fontPath = fontPathMap.get(fontName); + if (fontPath == null) { + return null; + } + } + InputStream inputStream = null; + try { + inputStream = applicationContext.getResource(fontPath).getInputStream(); + java.awt.Font font = java.awt.Font.createFont(java.awt.Font.TRUETYPE_FONT, inputStream); + return font.deriveFont(fontStyle, size); + } catch (Exception e) { + throw new ReportException(e); + } finally { + IOUtils.closeQuietly(inputStream); + } + } + + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + FontBuilder.applicationContext = applicationContext; + GraphicsEnvironment environment = GraphicsEnvironment.getLocalGraphicsEnvironment(); + String[] fontNames = environment.getAvailableFontFamilyNames(); + for (String name : fontNames) { + systemFontNameList.add(name); + } + Collection fontRegisters = applicationContext.getBeansOfType(FontRegister.class).values(); + for (FontRegister fontReg : fontRegisters) { + String fontName = fontReg.getFontName(); + String fontPath = fontReg.getFontPath(); + if (StringUtils.isEmpty(fontPath) || StringUtils.isEmpty(fontName)) { + continue; + } + try { + BaseFont baseFont = getIdentityFont(fontName, fontPath, applicationContext); + if (baseFont == null) { + throw new ReportComputeException("Font " + fontPath + " does not exist"); + } + fontMap.put(fontName, baseFont); + } catch (Exception e) { + e.printStackTrace(); + throw new ReportComputeException(e); + } + } + } + + private BaseFont getIdentityFont(String fontFamily, String fontPath, ApplicationContext applicationContext) throws DocumentException, IOException { + if (!fontPath.startsWith(ApplicationContext.CLASSPATH_URL_PREFIX)) { + fontPath = ApplicationContext.CLASSPATH_URL_PREFIX + fontPath; + } + String fontName = fontPath; + int lastSlashPos = fontPath.lastIndexOf("/"); + if (lastSlashPos != -1) { + fontName = fontPath.substring(lastSlashPos + 1, fontPath.length()); + } + if (fontName.toLowerCase().endsWith(".ttc")) { + fontName = fontName + ",0"; + } + InputStream inputStream = null; + try { + fontPathMap.put(fontFamily, fontPath); + inputStream = applicationContext.getResource(fontPath).getInputStream(); + byte[] bytes = IOUtils.toByteArray(inputStream); + BaseFont baseFont = BaseFont.createFont(fontName, BaseFont.IDENTITY_H, BaseFont.EMBEDDED, true, bytes, null); + baseFont.setSubset(true); + return baseFont; + } finally { + if (inputStream != null) inputStream.close(); + } + } +} diff --git a/blade-starter-report/src/main/java/com/bstek/ureport/parser/SlashBuilder.java b/blade-starter-report/src/main/java/com/bstek/ureport/parser/SlashBuilder.java new file mode 100644 index 0000000..a191e84 --- /dev/null +++ b/blade-starter-report/src/main/java/com/bstek/ureport/parser/SlashBuilder.java @@ -0,0 +1,223 @@ +/******************************************************************************* + * Copyright 2017 Bstek + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + ******************************************************************************/ +package com.bstek.ureport.parser; + +import com.bstek.ureport.cache.ResourceCache; +import com.bstek.ureport.definition.*; +import com.bstek.ureport.definition.value.Slash; +import com.bstek.ureport.definition.value.SlashValue; +import com.bstek.ureport.exception.ReportComputeException; +import com.bstek.ureport.utils.UnitUtils; +import org.springblade.core.tool.utils.Base64Util; + +import javax.imageio.ImageIO; +import javax.imageio.stream.MemoryCacheImageOutputStream; +import java.awt.*; +import java.awt.geom.AffineTransform; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.List; + +/** + * @author Jacky.gao + * @since 2017年3月17日 + */ +public class SlashBuilder { + public void buildSlashImage(CellDefinition cell, ReportDefinition report) { + int rowNumber = cell.getRowNumber(); + int colNumber = cell.getColumnNumber(); + int rowSpan = cell.getRowSpan(); + int colSpan = cell.getColSpan(); + int verticalBorderWidth = 0, horizontalBorderWidth = 0; + CellStyle cellStyle = cell.getCellStyle(); + if (cellStyle.getLeftBorder() != null) { + verticalBorderWidth += cellStyle.getLeftBorder().getWidth(); + } + if (cellStyle.getRightBorder() != null) { + verticalBorderWidth += cellStyle.getRightBorder().getWidth(); + } + if (cellStyle.getTopBorder() != null) { + horizontalBorderWidth = cellStyle.getTopBorder().getWidth(); + } + if (cellStyle.getBottomBorder() != null) { + horizontalBorderWidth = cellStyle.getBottomBorder().getWidth(); + } + int width = 0; + int height = 0; + if (rowSpan == 0) { + rowSpan = 1; + } + if (colSpan == 0) { + colSpan = 1; + } + List columns = report.getColumns(); + List rows = report.getRows(); + for (int i = colNumber; i < (colNumber + colSpan); i++) { + ColumnDefinition col = columns.get(i - 1); + width += UnitUtils.pointToPixel(col.getWidth()); + } + for (int i = rowNumber; i < (rowNumber + rowSpan); i++) { + RowDefinition row = rows.get(i - 1); + height += UnitUtils.pointToPixel(row.getHeight()); + } + width -= horizontalBorderWidth; + height -= verticalBorderWidth; + SlashValue content = (SlashValue) cell.getValue(); + BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_BGR); + Graphics2D g = (Graphics2D) image.getGraphics(); + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB); + g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + + Font font = cellStyle.getFont(); + g.setFont(font); + g.setStroke(new BasicStroke(1f)); + String bgColor = cellStyle.getBgcolor(); + if (bgColor == null) { + bgColor = "255,255,255"; + } + g.setColor(getColor(bgColor)); + g.fillRect(0, 0, width, height); + AffineTransform transform = g.getTransform(); + int allRowHeight = 0; + int index = 0; + String lc = cellStyle.getForecolor(); + if (lc == null) { + lc = "0,0,0"; + } + Color lineColor = getColor(lc); + String fc = cellStyle.getForecolor(); + if (fc == null) { + fc = "0,0,0"; + } + Color fontColor = getColor(fc); + for (int i = rowNumber; i < (rowNumber + rowSpan); i++) { + Slash slash = getSlash(content, index); + if (slash == null) { + break; + } + String text = slash.getText(); + if (text == null) { + break; + } + RowDefinition row = rows.get(i - 1); + int rowHeight = UnitUtils.pointToPixel(row.getHeight()); + g.setColor(fontColor); + int x = slash.getX(); + int y = slash.getY(); + g.rotate(Math.toRadians(slash.getDegree()), x, y); + g.drawString(text, x, y); + g.setTransform(transform); + g.setColor(lineColor); + int h = allRowHeight + rowHeight; + if (i == (rowNumber + rowSpan - 1)) { + h = allRowHeight + (rowHeight / 3) * 2; + } + g.drawLine(0, 0, width, h); + allRowHeight += rowHeight; + index++; + } + Slash slash = getSlash(content, index); + if (slash != null) { + String text = slash.getText(); + if (text != null) { + int x = slash.getX(); + int y = slash.getY(); + g.rotate(Math.toRadians(slash.getDegree()), x, y); + g.setColor(fontColor); + g.drawString(text, x, y); + g.setTransform(transform); + index++; + } + } + if (colSpan > 0) { + colSpan--; + } + int colNumberStart = colNumber + colSpan; + for (int i = colNumberStart; i > (colNumber - 1); i--) { + slash = getSlash(content, index); + if (slash == null) { + break; + } + String text = slash.getText(); + if (text == null) { + break; + } + int x = slash.getX(); + int y = slash.getY(); + g.rotate(Math.toRadians(slash.getDegree()), x, y); + g.setColor(fontColor); + g.drawString(text, x, y); + g.setTransform(transform); + ColumnDefinition col = columns.get(i - 1); + int colWidth = UnitUtils.pointToPixel(col.getWidth()); + int w = width; + if (i == colNumberStart) { + w = width - colWidth / 3; + } + g.setColor(lineColor); + g.drawLine(0, 0, w, height); + width -= colWidth; + index++; + } + byte[] imageBytes = null; + ByteArrayOutputStream byteOutput = new ByteArrayOutputStream(); + MemoryCacheImageOutputStream memoryImage = new MemoryCacheImageOutputStream(byteOutput); + try { + ImageIO.write(image, "png", memoryImage); + imageBytes = byteOutput.toByteArray(); + String base64Data = Base64Util.encodeToString(imageBytes); + content.setBase64Data(base64Data); + } catch (Exception ex) { + throw new ReportComputeException(ex); + } finally { + try { + if (memoryImage != null) { + memoryImage.close(); + } + if (byteOutput != null) { + byteOutput.close(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + g.dispose(); + String imageByteKey = buildKey(report.getReportFullName(), cell.getName()); + ResourceCache.putObject(imageByteKey, imageBytes); + } + + private Slash getSlash(SlashValue content, int index) { + List slashes = content.getSlashes(); + if (index < slashes.size()) { + return slashes.get(index); + } + return null; + } + + private Color getColor(String text) { + if (text == null) { + return null; + } + String[] str = text.split(","); + return new Color(Integer.valueOf(str[0]), Integer.valueOf(str[1]), Integer.valueOf(str[2])); + } + + public static String buildKey(String reportFullName, String cellName) { + return "slash-" + reportFullName + "-" + cellName; + } +} diff --git a/blade-starter-report/src/main/java/com/bstek/ureport/provider/image/DefaultImageProvider.java b/blade-starter-report/src/main/java/com/bstek/ureport/provider/image/DefaultImageProvider.java new file mode 100644 index 0000000..d9261be --- /dev/null +++ b/blade-starter-report/src/main/java/com/bstek/ureport/provider/image/DefaultImageProvider.java @@ -0,0 +1,71 @@ +/******************************************************************************* + * Copyright 2017 Bstek + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + ******************************************************************************/ +package com.bstek.ureport.provider.image; + +import com.bstek.ureport.exception.ReportComputeException; +import jakarta.servlet.ServletContext; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.util.ResourceUtils; +import org.springframework.web.context.ServletContextAware; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * @author Jacky.gao + * @since 2017年3月6日 + */ +public class DefaultImageProvider implements ImageProvider, ApplicationContextAware, ServletContextAware { + private ApplicationContext applicationContext; + private String baseWebPath; + + @Override + public InputStream getImage(String path) { + try { + if (path.startsWith(ResourceUtils.CLASSPATH_URL_PREFIX) || path.startsWith("/WEB-INF")) { + return applicationContext.getResource(path).getInputStream(); + } else { + path = baseWebPath + path; + return new FileInputStream(path); + } + } catch (IOException e) { + throw new ReportComputeException(e); + } + } + + @Override + public boolean support(String path) { + if (path.startsWith(ResourceUtils.CLASSPATH_URL_PREFIX)) { + return true; + } else if (baseWebPath != null && (path.startsWith("/") || path.startsWith("/WEB-INF"))) { + return true; + } + return false; + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + + @Override + public void setServletContext(ServletContext servletContext) { + this.baseWebPath = servletContext.getRealPath("/"); + } +} diff --git a/blade-starter-report/src/main/java/com/bstek/ureport/provider/report/file/FileReportProvider.java b/blade-starter-report/src/main/java/com/bstek/ureport/provider/report/file/FileReportProvider.java new file mode 100644 index 0000000..c62ca5b --- /dev/null +++ b/blade-starter-report/src/main/java/com/bstek/ureport/provider/report/file/FileReportProvider.java @@ -0,0 +1,147 @@ +/******************************************************************************* + * Copyright 2017 Bstek + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + ******************************************************************************/ +package com.bstek.ureport.provider.report.file; + +import com.bstek.ureport.exception.ReportException; +import com.bstek.ureport.provider.report.ReportFile; +import com.bstek.ureport.provider.report.ReportProvider; +import jakarta.servlet.ServletContext; +import org.apache.commons.io.IOUtils; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.web.context.WebApplicationContext; + +import java.io.*; +import java.util.*; + +/** + * @author Jacky.gao + * @since 2017年2月11日 + */ +public class FileReportProvider implements ReportProvider, ApplicationContextAware { + private String prefix = "file:"; + private String fileStoreDir; + private boolean disabled; + + @Override + public InputStream loadReport(String file) { + if (file.startsWith(prefix)) { + file = file.substring(prefix.length(), file.length()); + } + String fullPath = fileStoreDir + "/" + file; + try { + return new FileInputStream(fullPath); + } catch (FileNotFoundException e) { + throw new ReportException(e); + } + } + + @Override + public void deleteReport(String file) { + if (file.startsWith(prefix)) { + file = file.substring(prefix.length(), file.length()); + } + String fullPath = fileStoreDir + "/" + file; + File f = new File(fullPath); + if (f.exists()) { + f.delete(); + } + } + + @Override + public List getReportFiles() { + File file = new File(fileStoreDir); + List list = new ArrayList(); + for (File f : file.listFiles()) { + Calendar calendar = Calendar.getInstance(); + calendar.setTimeInMillis(f.lastModified()); + list.add(new ReportFile(f.getName(), calendar.getTime())); + } + Collections.sort(list, new Comparator() { + @Override + public int compare(ReportFile f1, ReportFile f2) { + return f2.getUpdateDate().compareTo(f1.getUpdateDate()); + } + }); + return list; + } + + @Override + public String getName() { + return "服务器文件系统"; + } + + @Override + public void saveReport(String file, String content) { + if (file.startsWith(prefix)) { + file = file.substring(prefix.length(), file.length()); + } + String fullPath = fileStoreDir + "/" + file; + FileOutputStream outStream = null; + try { + outStream = new FileOutputStream(new File(fullPath)); + IOUtils.write(content, outStream, "utf-8"); + } catch (Exception ex) { + throw new ReportException(ex); + } finally { + if (outStream != null) { + try { + outStream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + } + + @Override + public boolean disabled() { + return disabled; + } + + public void setDisabled(boolean disabled) { + this.disabled = disabled; + } + + public void setFileStoreDir(String fileStoreDir) { + this.fileStoreDir = fileStoreDir; + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + File file = new File(fileStoreDir); + if (file.exists()) { + return; + } + if (applicationContext instanceof WebApplicationContext) { + WebApplicationContext context = (WebApplicationContext) applicationContext; + ServletContext servletContext = context.getServletContext(); + String basePath = servletContext.getRealPath("/"); + fileStoreDir = basePath + fileStoreDir; + file = new File(fileStoreDir); + if (!file.exists()) { + file.mkdirs(); + } + } + } + + @Override + public String getPrefix() { + return prefix; + } +} diff --git a/blade-starter-report/src/main/java/com/bstek/ureport/utils/ImageUtils.java b/blade-starter-report/src/main/java/com/bstek/ureport/utils/ImageUtils.java new file mode 100644 index 0000000..9ce9e6f --- /dev/null +++ b/blade-starter-report/src/main/java/com/bstek/ureport/utils/ImageUtils.java @@ -0,0 +1,81 @@ +/******************************************************************************* + * Copyright 2017 Bstek + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + ******************************************************************************/ +package com.bstek.ureport.utils; + +import com.bstek.ureport.exception.ReportComputeException; +import com.bstek.ureport.image.ChartImageProcessor; +import com.bstek.ureport.image.ImageProcessor; +import com.bstek.ureport.image.ImageType; +import com.bstek.ureport.image.StaticImageProcessor; +import org.apache.commons.io.IOUtils; +import org.springblade.core.tool.utils.Base64Util; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; + +/** + * @author Jacky.gao + * @since 2017年3月20日 + */ +public class ImageUtils { + private static Map> imageProcessorMap = new HashMap>(); + + static { + StaticImageProcessor staticImageProcessor = new StaticImageProcessor(); + imageProcessorMap.put(ImageType.image, staticImageProcessor); + ChartImageProcessor chartImageProcessor = new ChartImageProcessor(); + imageProcessorMap.put(ImageType.chart, chartImageProcessor); + } + + public static InputStream base64DataToInputStream(String base64Data) { + byte[] bytes = Base64Util.decodeFromString(base64Data); + ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes); + return inputStream; + } + + @SuppressWarnings("unchecked") + public static String getImageBase64Data(ImageType type, Object data, int width, int height) { + ImageProcessor targetProcessor = (ImageProcessor) imageProcessorMap.get(type); + if (targetProcessor == null) { + throw new ReportComputeException("Unknow image type :" + type); + } + InputStream inputStream = targetProcessor.getImage(data); + try { + if (width > 0 && height > 0) { + BufferedImage inputImage = ImageIO.read(inputStream); + BufferedImage outputImage = new BufferedImage(width, height, BufferedImage.TYPE_USHORT_565_RGB); + Graphics2D g = outputImage.createGraphics(); + g.drawImage(inputImage, 0, 0, width, height, null); + g.dispose(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ImageIO.write(outputImage, "png", outputStream); + inputStream = new ByteArrayInputStream(outputStream.toByteArray()); + } + byte[] bytes = IOUtils.toByteArray(inputStream); + return Base64Util.encodeToString(bytes); + } catch (Exception ex) { + throw new ReportComputeException(ex); + } finally { + IOUtils.closeQuietly(inputStream); + } + } +} diff --git a/blade-starter-report/src/main/java/org/springblade/core/report/config/ReportConfiguration.java b/blade-starter-report/src/main/java/org/springblade/core/report/config/ReportConfiguration.java new file mode 100644 index 0000000..49746e4 --- /dev/null +++ b/blade-starter-report/src/main/java/org/springblade/core/report/config/ReportConfiguration.java @@ -0,0 +1,75 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.report.config; + +import com.bstek.ureport.UReportPropertyPlaceholderConfigurer; +import com.bstek.ureport.console.UReportServlet; +import com.bstek.ureport.provider.report.ReportProvider; +import org.springblade.core.report.props.ReportDatabaseProperties; +import org.springblade.core.report.props.ReportProperties; +import org.springblade.core.report.provider.DatabaseProvider; +import org.springblade.core.report.provider.ReportPlaceholderProvider; +import org.springblade.core.report.service.IReportFileService; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.web.servlet.ServletRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ImportResource; +import org.springframework.core.annotation.Order; + +import jakarta.servlet.Servlet; + +/** + * UReport配置类 + * + * @author Chill + */ +@Order +@AutoConfiguration +@ConditionalOnProperty(value = "report.enabled", havingValue = "true", matchIfMissing = true) +@EnableConfigurationProperties({ReportProperties.class, ReportDatabaseProperties.class}) +@ImportResource("classpath:ureport-console-context.xml") +public class ReportConfiguration { + + @Bean + public ServletRegistrationBean registrationBean() { + return new ServletRegistrationBean<>(new UReportServlet(), "/ureport/*"); + } + + @Bean + public UReportPropertyPlaceholderConfigurer uReportPropertyPlaceholderConfigurer(ReportProperties properties) { + return new ReportPlaceholderProvider(properties); + } + + @Bean + @ConditionalOnMissingBean + public ReportProvider reportProvider(ReportDatabaseProperties properties, IReportFileService service) { + return new DatabaseProvider(properties, service); + } + +} diff --git a/blade-starter-report/src/main/java/org/springblade/core/report/datasource/ReportDataSource.java b/blade-starter-report/src/main/java/org/springblade/core/report/datasource/ReportDataSource.java new file mode 100644 index 0000000..c6e0e34 --- /dev/null +++ b/blade-starter-report/src/main/java/org/springblade/core/report/datasource/ReportDataSource.java @@ -0,0 +1,64 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.report.datasource; + +import com.bstek.ureport.definition.datasource.BuildinDatasource; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; + +/** + * UReport数据源配置 + * + * @author Chill + */ +@Slf4j +@AllArgsConstructor +public class ReportDataSource implements BuildinDatasource { + private static final String NAME = "ReportDataSource"; + + private final DataSource dataSource; + + @Override + public String name() { + return NAME; + } + + @Override + public Connection getConnection() { + try { + return dataSource.getConnection(); + } catch (SQLException e) { + log.error("report数据源链接失败"); + e.printStackTrace(); + } + return null; + } + +} diff --git a/blade-starter-report/src/main/java/org/springblade/core/report/endpoint/ReportBootEndpoint.java b/blade-starter-report/src/main/java/org/springblade/core/report/endpoint/ReportBootEndpoint.java new file mode 100644 index 0000000..6e16a3a --- /dev/null +++ b/blade-starter-report/src/main/java/org/springblade/core/report/endpoint/ReportBootEndpoint.java @@ -0,0 +1,48 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.report.endpoint; + +import io.swagger.v3.oas.annotations.Hidden; +import org.springblade.core.launch.constant.AppConstant; +import org.springblade.core.report.service.IReportFileService; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * UReport Boot版 API端点 + * + * @author Chill + */ +@Hidden +@RestController +@RequestMapping(AppConstant.APPLICATION_REPORT_NAME + "/report/rest") +public class ReportBootEndpoint extends ReportEndpoint { + + public ReportBootEndpoint(IReportFileService service) { + super(service); + } + +} diff --git a/blade-starter-report/src/main/java/org/springblade/core/report/endpoint/ReportEndpoint.java b/blade-starter-report/src/main/java/org/springblade/core/report/endpoint/ReportEndpoint.java new file mode 100644 index 0000000..d4a9f3b --- /dev/null +++ b/blade-starter-report/src/main/java/org/springblade/core/report/endpoint/ReportEndpoint.java @@ -0,0 +1,81 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.report.endpoint; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Hidden; +import lombok.AllArgsConstructor; +import org.springblade.core.mp.support.Condition; +import org.springblade.core.mp.support.Query; +import org.springblade.core.report.entity.ReportFileEntity; +import org.springblade.core.report.service.IReportFileService; +import org.springblade.core.tool.api.R; +import org.springblade.core.tool.utils.Func; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +/** + * UReport API端点 + * + * @author Chill + */ +@Hidden +@RestController +@AllArgsConstructor +@RequestMapping("/report/rest") +public class ReportEndpoint { + + private final IReportFileService service; + + /** + * 详情 + */ + @GetMapping("/detail") + public R detail(ReportFileEntity file) { + ReportFileEntity detail = service.getOne(Condition.getQueryWrapper(file)); + return R.data(detail); + } + + /** + * 分页 + */ + @GetMapping("/list") + public R> list(@RequestParam Map file, Query query) { + IPage pages = service.page(Condition.getPage(query), Condition.getQueryWrapper(file, ReportFileEntity.class)); + return R.data(pages); + } + + /** + * 删除 + */ + @PostMapping("/remove") + public R remove(@RequestParam String ids) { + boolean temp = service.removeByIds(Func.toLongList(ids)); + return R.status(temp); + } + +} diff --git a/blade-starter-report/src/main/java/org/springblade/core/report/entity/ReportFileEntity.java b/blade-starter-report/src/main/java/org/springblade/core/report/entity/ReportFileEntity.java new file mode 100644 index 0000000..c416fb4 --- /dev/null +++ b/blade-starter-report/src/main/java/org/springblade/core/report/entity/ReportFileEntity.java @@ -0,0 +1,77 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.report.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableLogic; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Date; + +/** + * UReport实体类 + * + * @author Chill + */ +@Data +@TableName("blade_report_file") +public class ReportFileEntity implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 主键 + */ + @TableId(value = "id", type = IdType.ASSIGN_ID) + private Long id; + /** + * 文件名 + */ + private String name; + /** + * 文件内容 + */ + private byte[] content; + /** + * 创建时间 + */ + private Date createTime; + /** + * 更新时间 + */ + private Date updateTime; + /** + * 是否已删除 + */ + @TableLogic + private Integer isDeleted; + +} diff --git a/blade-starter-report/src/main/java/org/springblade/core/report/mapper/ReportFileMapper.java b/blade-starter-report/src/main/java/org/springblade/core/report/mapper/ReportFileMapper.java new file mode 100644 index 0000000..6d15baa --- /dev/null +++ b/blade-starter-report/src/main/java/org/springblade/core/report/mapper/ReportFileMapper.java @@ -0,0 +1,37 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.report.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.springblade.core.report.entity.ReportFileEntity; + +/** + * UReport Mapper + * + * @author Chill + */ +public interface ReportFileMapper extends BaseMapper { +} diff --git a/blade-starter-report/src/main/java/org/springblade/core/report/props/ReportDatabaseProperties.java b/blade-starter-report/src/main/java/org/springblade/core/report/props/ReportDatabaseProperties.java new file mode 100644 index 0000000..f6705f6 --- /dev/null +++ b/blade-starter-report/src/main/java/org/springblade/core/report/props/ReportDatabaseProperties.java @@ -0,0 +1,42 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.report.props; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * UReport配置类 + * + * @author Chill + */ +@Data +@ConfigurationProperties(prefix = "report.database.provider") +public class ReportDatabaseProperties { + private String name = "数据库文件系统"; + private String prefix = "blade-"; + private boolean disabled = false; +} diff --git a/blade-starter-report/src/main/java/org/springblade/core/report/props/ReportProperties.java b/blade-starter-report/src/main/java/org/springblade/core/report/props/ReportProperties.java new file mode 100644 index 0000000..60e4ba6 --- /dev/null +++ b/blade-starter-report/src/main/java/org/springblade/core/report/props/ReportProperties.java @@ -0,0 +1,45 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.report.props; + +import lombok.Data; +import org.springblade.core.tool.utils.StringPool; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * UReport配置类 + * + * @author Chill + */ +@Data +@ConfigurationProperties(prefix = "report") +public class ReportProperties { + private Boolean enabled = true; + private Boolean disableHttpSessionReportCache = false; + private Boolean disableFileProvider = true; + private String fileStoreDir = StringPool.EMPTY; + private Boolean debug = false; +} diff --git a/blade-starter-report/src/main/java/org/springblade/core/report/provider/DatabaseProvider.java b/blade-starter-report/src/main/java/org/springblade/core/report/provider/DatabaseProvider.java new file mode 100644 index 0000000..c8b6f91 --- /dev/null +++ b/blade-starter-report/src/main/java/org/springblade/core/report/provider/DatabaseProvider.java @@ -0,0 +1,120 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.report.provider; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.bstek.ureport.provider.report.ReportFile; +import com.bstek.ureport.provider.report.ReportProvider; +import lombok.AllArgsConstructor; +import org.springblade.core.tool.constant.BladeConstant; +import org.springblade.core.tool.utils.DateUtil; +import org.springblade.core.report.entity.ReportFileEntity; +import org.springblade.core.report.props.ReportDatabaseProperties; +import org.springblade.core.report.service.IReportFileService; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * 数据库文件处理 + * + * @author Chill + */ +@AllArgsConstructor +public class DatabaseProvider implements ReportProvider { + + private final ReportDatabaseProperties properties; + private final IReportFileService service; + + @Override + public InputStream loadReport(String file) { + ReportFileEntity reportFileEntity = service.getOne(Wrappers.lambdaQuery().eq(ReportFileEntity::getName, getFileName(file))); + byte[] content = reportFileEntity.getContent(); + return new ByteArrayInputStream(content); + } + + @Override + public void deleteReport(String file) { + service.remove(Wrappers.lambdaUpdate().eq(ReportFileEntity::getName, getFileName(file))); + } + + @Override + public List getReportFiles() { + List list = service.list(); + List reportFiles = new ArrayList<>(); + list.forEach(reportFileEntity -> reportFiles.add(new ReportFile(reportFileEntity.getName(), reportFileEntity.getUpdateTime()))); + return reportFiles; + } + + @Override + public void saveReport(String file, String content) { + String fileName = getFileName(file); + ReportFileEntity reportFileEntity = service.getOne(Wrappers.lambdaQuery().eq(ReportFileEntity::getName, fileName)); + Date now = DateUtil.now(); + if (reportFileEntity == null) { + reportFileEntity = new ReportFileEntity(); + reportFileEntity.setName(fileName); + reportFileEntity.setContent(content.getBytes()); + reportFileEntity.setCreateTime(now); + reportFileEntity.setIsDeleted(BladeConstant.DB_NOT_DELETED); + } else { + reportFileEntity.setContent(content.getBytes()); + } + reportFileEntity.setUpdateTime(now); + service.saveOrUpdate(reportFileEntity); + } + + @Override + public String getName() { + return properties.getName(); + } + + @Override + public boolean disabled() { + return properties.isDisabled(); + } + + @Override + public String getPrefix() { + return properties.getPrefix(); + } + + /** + * 获取标准格式文件名 + * + * @param name 原文件名 + */ + private String getFileName(String name) { + if (name.startsWith(getPrefix())) { + name = name.substring(getPrefix().length()); + } + return name; + } + +} diff --git a/blade-starter-report/src/main/java/org/springblade/core/report/provider/ReportPlaceholderProvider.java b/blade-starter-report/src/main/java/org/springblade/core/report/provider/ReportPlaceholderProvider.java new file mode 100644 index 0000000..f0d4c23 --- /dev/null +++ b/blade-starter-report/src/main/java/org/springblade/core/report/provider/ReportPlaceholderProvider.java @@ -0,0 +1,49 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.report.provider; + +import com.bstek.ureport.UReportPropertyPlaceholderConfigurer; +import org.springblade.core.report.props.ReportProperties; + +import java.util.Properties; + +/** + * UReport自定义配置 + * + * @author Chill + */ +public class ReportPlaceholderProvider extends UReportPropertyPlaceholderConfigurer { + + public ReportPlaceholderProvider(ReportProperties properties) { + Properties props = new Properties(); + props.setProperty("ureport.disableHttpSessionReportCache", properties.getDisableHttpSessionReportCache().toString()); + props.setProperty("ureport.disableFileProvider", properties.getDisableFileProvider().toString()); + props.setProperty("ureport.fileStoreDir", properties.getFileStoreDir()); + props.setProperty("ureport.debug", properties.getDebug().toString()); + this.setProperties(props); + } + +} diff --git a/blade-starter-report/src/main/java/org/springblade/core/report/service/IReportFileService.java b/blade-starter-report/src/main/java/org/springblade/core/report/service/IReportFileService.java new file mode 100644 index 0000000..338bf03 --- /dev/null +++ b/blade-starter-report/src/main/java/org/springblade/core/report/service/IReportFileService.java @@ -0,0 +1,37 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.report.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import org.springblade.core.report.entity.ReportFileEntity; + +/** + * UReport Service + * + * @author Chill + */ +public interface IReportFileService extends IService { +} diff --git a/blade-starter-report/src/main/java/org/springblade/core/report/service/impl/ReportFileServiceImpl.java b/blade-starter-report/src/main/java/org/springblade/core/report/service/impl/ReportFileServiceImpl.java new file mode 100644 index 0000000..1e72947 --- /dev/null +++ b/blade-starter-report/src/main/java/org/springblade/core/report/service/impl/ReportFileServiceImpl.java @@ -0,0 +1,41 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.report.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.springblade.core.report.entity.ReportFileEntity; +import org.springblade.core.report.mapper.ReportFileMapper; +import org.springblade.core.report.service.IReportFileService; +import org.springframework.stereotype.Service; + +/** + * UReport Service + * + * @author Chill + */ +@Service +public class ReportFileServiceImpl extends ServiceImpl implements IReportFileService { +} diff --git a/blade-starter-sharding/pom.xml b/blade-starter-sharding/pom.xml new file mode 100644 index 0000000..e730f64 --- /dev/null +++ b/blade-starter-sharding/pom.xml @@ -0,0 +1,56 @@ + + + 4.0.0 + + org.springblade + BladeX-Tool + ${revision} + + + blade-starter-sharding + ${project.artifactId} + ${project.parent.version} + jar + + + + + org.springblade + blade-core-launch + + + + com.alibaba + druid-spring-boot-3-starter + provided + + + + com.baomidou + dynamic-datasource-spring-boot3-starter + + + + org.apache.shardingsphere + shardingsphere-jdbc-core-spring-boot-starter + + + + org.yaml + snakeyaml + + + org.antlr + antlr4-runtime + + + + org.springblade + blade-core-auto + provided + + + + diff --git a/blade-starter-sharding/src/main/java/org/apache/shardingsphere/elasticjob/infra/yaml/representer/ElasticJobYamlRepresenter.java b/blade-starter-sharding/src/main/java/org/apache/shardingsphere/elasticjob/infra/yaml/representer/ElasticJobYamlRepresenter.java new file mode 100644 index 0000000..12e16ca --- /dev/null +++ b/blade-starter-sharding/src/main/java/org/apache/shardingsphere/elasticjob/infra/yaml/representer/ElasticJobYamlRepresenter.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shardingsphere.elasticjob.infra.yaml.representer; + +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.introspector.Property; +import org.yaml.snakeyaml.nodes.NodeTuple; +import org.yaml.snakeyaml.nodes.Tag; +import org.yaml.snakeyaml.representer.Representer; + +/** + * ElasticJob YAML representer. + */ +public final class ElasticJobYamlRepresenter extends Representer { + public ElasticJobYamlRepresenter(final DumperOptions options) { + super(options); + } + + @Override + protected NodeTuple representJavaBeanProperty(final Object javaBean, final Property property, final Object propertyValue, final Tag customTag) { + return new DefaultYamlTupleProcessor().process(super.representJavaBeanProperty(javaBean, property, propertyValue, customTag)); + } +} diff --git a/blade-starter-sharding/src/main/java/org/apache/shardingsphere/infra/util/yaml/YamlEngine.java b/blade-starter-sharding/src/main/java/org/apache/shardingsphere/infra/util/yaml/YamlEngine.java new file mode 100644 index 0000000..1cd7700 --- /dev/null +++ b/blade-starter-sharding/src/main/java/org/apache/shardingsphere/infra/util/yaml/YamlEngine.java @@ -0,0 +1,110 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shardingsphere.infra.util.yaml; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.apache.shardingsphere.infra.util.yaml.constructor.ShardingSphereYamlConstructor; +import org.apache.shardingsphere.infra.util.yaml.representer.ShardingSphereYamlRepresenter; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.representer.Representer; + +import java.io.*; +import java.util.Collection; + +/** + * YAML engine. + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class YamlEngine { + + /** + * Unmarshal YAML. + * + * @param yamlFile YAML file + * @param classType class type + * @param type of class + * @return object from YAML + * @throws IOException IO Exception + */ + public static T unmarshal(final File yamlFile, final Class classType) throws IOException { + try ( + FileInputStream fileInputStream = new FileInputStream(yamlFile); + InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream)) { + return new Yaml(new ShardingSphereYamlConstructor(classType)).loadAs(inputStreamReader, classType); + } + } + + /** + * Unmarshal YAML. + * + * @param yamlBytes YAML bytes + * @param classType class type + * @param type of class + * @return object from YAML + * @throws IOException IO Exception + */ + public static T unmarshal(final byte[] yamlBytes, final Class classType) throws IOException { + try (InputStream inputStream = new ByteArrayInputStream(yamlBytes)) { + return new Yaml(new ShardingSphereYamlConstructor(classType)).loadAs(inputStream, classType); + } + } + + /** + * Unmarshal YAML. + * + * @param yamlContent YAML content + * @param classType class type + * @param type of class + * @return object from YAML + */ + public static T unmarshal(final String yamlContent, final Class classType) { + return new Yaml(new ShardingSphereYamlConstructor(classType)).loadAs(yamlContent, classType); + } + + /** + * Unmarshal YAML. + * + * @param yamlContent YAML content + * @param classType class type + * @param skipMissingProps true if missing properties should be skipped, false otherwise + * @param type of class + * @return object from YAML + */ + public static T unmarshal(final String yamlContent, final Class classType, final boolean skipMissingProps) { + Representer representer = new Representer(new DumperOptions()); + representer.getPropertyUtils().setSkipMissingProperties(skipMissingProps); + return new Yaml(new ShardingSphereYamlConstructor(classType), representer).loadAs(yamlContent, classType); + } + + /** + * Marshal YAML. + * + * @param value object to be marshaled + * @return YAML content + */ + public static String marshal(final Object value) { + DumperOptions dumperOptions = new DumperOptions(); + dumperOptions.setLineBreak(DumperOptions.LineBreak.getPlatformLineBreak()); + if (value instanceof Collection) { + return new Yaml(new ShardingSphereYamlRepresenter(dumperOptions), dumperOptions).dumpAs(value, null, DumperOptions.FlowStyle.BLOCK); + } + return new Yaml(new ShardingSphereYamlRepresenter(dumperOptions), dumperOptions).dumpAsMap(value); + } +} diff --git a/blade-starter-sharding/src/main/java/org/apache/shardingsphere/infra/util/yaml/representer/ShardingSphereYamlRepresenter.java b/blade-starter-sharding/src/main/java/org/apache/shardingsphere/infra/util/yaml/representer/ShardingSphereYamlRepresenter.java new file mode 100644 index 0000000..2c108cd --- /dev/null +++ b/blade-starter-sharding/src/main/java/org/apache/shardingsphere/infra/util/yaml/representer/ShardingSphereYamlRepresenter.java @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shardingsphere.infra.util.yaml.representer; + +import org.apache.shardingsphere.infra.util.yaml.representer.processor.DefaultYamlTupleProcessor; +import org.apache.shardingsphere.infra.util.yaml.representer.processor.ShardingSphereYamlTupleProcessor; +import org.apache.shardingsphere.infra.util.yaml.representer.processor.ShardingSphereYamlTupleProcessorFactory; +import org.apache.shardingsphere.infra.util.yaml.shortcuts.ShardingSphereYamlShortcutsFactory; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.introspector.Property; +import org.yaml.snakeyaml.nodes.Node; +import org.yaml.snakeyaml.nodes.NodeTuple; +import org.yaml.snakeyaml.nodes.Tag; +import org.yaml.snakeyaml.representer.Representer; + +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; + +/** + * ShardingSphere YAML representer. + */ +public final class ShardingSphereYamlRepresenter extends Representer { + + public ShardingSphereYamlRepresenter(DumperOptions options) { + super(options); + ShardingSphereYamlShortcutsFactory.getAllYamlShortcuts().forEach((key, value) -> addClassTag(value, new Tag(key))); + } + + @Override + protected NodeTuple representJavaBeanProperty(final Object javaBean, final Property property, final Object propertyValue, final Tag customTag) { + NodeTuple nodeTuple = super.representJavaBeanProperty(javaBean, property, propertyValue, customTag); + for (ShardingSphereYamlTupleProcessor each : ShardingSphereYamlTupleProcessorFactory.getAllInstances()) { + if (property.getName().equals(each.getTupleName())) { + return each.process(nodeTuple); + } + } + return new DefaultYamlTupleProcessor().process(nodeTuple); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + @Override + protected Node representMapping(final Tag tag, final Map mapping, final DumperOptions.FlowStyle flowStyle) { + Map skippedEmptyValuesMapping = new LinkedHashMap<>(mapping.size(), 1); + for (Entry entry : mapping.entrySet()) { + if (entry.getValue() instanceof Collection && ((Collection) entry.getValue()).isEmpty()) { + continue; + } + if (entry.getValue() instanceof Map && ((Map) entry.getValue()).isEmpty()) { + continue; + } + skippedEmptyValuesMapping.put(entry.getKey(), entry.getValue()); + } + return super.representMapping(tag, skippedEmptyValuesMapping, flowStyle); + } +} diff --git a/blade-starter-sharding/src/main/java/org/springblade/core/sharding/ShardingDataSourceProvider.java b/blade-starter-sharding/src/main/java/org/springblade/core/sharding/ShardingDataSourceProvider.java new file mode 100644 index 0000000..95f5b8d --- /dev/null +++ b/blade-starter-sharding/src/main/java/org/springblade/core/sharding/ShardingDataSourceProvider.java @@ -0,0 +1,88 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.sharding; + +import com.baomidou.dynamic.datasource.creator.DataSourceProperty; +import com.baomidou.dynamic.datasource.creator.DefaultDataSourceCreator; +import com.baomidou.dynamic.datasource.provider.AbstractDataSourceProvider; +import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceProperties; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.sharding.constant.ShardingConstant; + +import javax.sql.DataSource; +import java.util.HashMap; +import java.util.Map; + +/** + * 分库分表数据源初始加载 + * + * @author Chill + */ +@Slf4j +public class ShardingDataSourceProvider extends AbstractDataSourceProvider { + + private final String driverClassName; + private final String url; + private final String username; + private final String password; + private final DynamicDataSourceProperties dynamicDataSourceProperties; + private final DataSource shardingSphereDataSource; + + public ShardingDataSourceProvider(DefaultDataSourceCreator dataSourceCreator, DynamicDataSourceProperties dynamicDataSourceProperties, String driverClassName, String url, String username, String password, DataSource shardingSphereDataSource) { + super(dataSourceCreator); + this.dynamicDataSourceProperties = dynamicDataSourceProperties; + this.driverClassName = driverClassName; + this.url = url; + this.username = username; + this.password = password; + this.shardingSphereDataSource = shardingSphereDataSource; + } + + @Override + public Map loadDataSources() { + // 构建数据源集合 + Map map = new HashMap<>(16); + // 构建主数据源 + DataSourceProperty masterProperty = new DataSourceProperty(); + masterProperty.setDriverClassName(driverClassName); + masterProperty.setUrl(url); + masterProperty.setUsername(username); + masterProperty.setPassword(password); + masterProperty.setDruid(dynamicDataSourceProperties.getDruid()); + map.put(dynamicDataSourceProperties.getPrimary(), masterProperty); + // 构建yml数据源 + Map datasource = dynamicDataSourceProperties.getDatasource(); + if (!datasource.isEmpty()) { + map.putAll(datasource); + } + // 构建分库分表数据源 + Map dataSourceMap = createDataSourceMap(map); + dataSourceMap.put(ShardingConstant.SHARDING_DATASOURCE_KEY, shardingSphereDataSource); + return dataSourceMap; + } + + +} diff --git a/blade-starter-sharding/src/main/java/org/springblade/core/sharding/ShardingUtil.java b/blade-starter-sharding/src/main/java/org/springblade/core/sharding/ShardingUtil.java new file mode 100644 index 0000000..483c812 --- /dev/null +++ b/blade-starter-sharding/src/main/java/org/springblade/core/sharding/ShardingUtil.java @@ -0,0 +1,49 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.sharding; + +import lombok.SneakyThrows; +import org.apache.shardingsphere.driver.api.yaml.YamlShardingSphereDataSourceFactory; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.Constructor; + +import javax.sql.DataSource; +import java.nio.charset.StandardCharsets; + +/** + * ShardingUtil + * + * @author Chill + */ +public class ShardingUtil { + + @SneakyThrows + public static DataSource createDataSource(String yamlConfig) { + return YamlShardingSphereDataSourceFactory.createDataSource(yamlConfig.getBytes(StandardCharsets.UTF_8)); + } + + +} diff --git a/blade-starter-sharding/src/main/java/org/springblade/core/sharding/annotation/ShardingDS.java b/blade-starter-sharding/src/main/java/org/springblade/core/sharding/annotation/ShardingDS.java new file mode 100644 index 0000000..3fc6159 --- /dev/null +++ b/blade-starter-sharding/src/main/java/org/springblade/core/sharding/annotation/ShardingDS.java @@ -0,0 +1,43 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.sharding.annotation; + +import com.baomidou.dynamic.datasource.annotation.DS; +import org.springblade.core.sharding.constant.ShardingConstant; + +import java.lang.annotation.*; + +/** + * 指定分库分表数据源切换 + * + * @author Chill + */ +@DS(ShardingConstant.SHARDING_DATASOURCE_KEY) +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface ShardingDS { +} diff --git a/blade-starter-sharding/src/main/java/org/springblade/core/sharding/config/ShardingConfiguration.java b/blade-starter-sharding/src/main/java/org/springblade/core/sharding/config/ShardingConfiguration.java new file mode 100644 index 0000000..71460d9 --- /dev/null +++ b/blade-starter-sharding/src/main/java/org/springblade/core/sharding/config/ShardingConfiguration.java @@ -0,0 +1,193 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.sharding.config; + +import com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceAutoConfigure; +import com.baomidou.dynamic.datasource.DynamicRoutingDataSource; +import com.baomidou.dynamic.datasource.annotation.DS; +import com.baomidou.dynamic.datasource.aop.DynamicDataSourceAnnotationAdvisor; +import com.baomidou.dynamic.datasource.aop.DynamicDataSourceAnnotationInterceptor; +import com.baomidou.dynamic.datasource.creator.DataSourceCreator; +import com.baomidou.dynamic.datasource.creator.DataSourceProperty; +import com.baomidou.dynamic.datasource.creator.DefaultDataSourceCreator; +import com.baomidou.dynamic.datasource.event.DataSourceInitEvent; +import com.baomidou.dynamic.datasource.event.EncDataSourceInitEvent; +import com.baomidou.dynamic.datasource.processor.DsJakartaHeaderProcessor; +import com.baomidou.dynamic.datasource.processor.DsJakartaSessionProcessor; +import com.baomidou.dynamic.datasource.processor.DsProcessor; +import com.baomidou.dynamic.datasource.processor.DsSpelExpressionProcessor; +import com.baomidou.dynamic.datasource.provider.DynamicDataSourceProvider; +import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceAutoConfiguration; +import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceCreatorAutoConfiguration; +import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceProperties; +import jakarta.annotation.Resource; +import lombok.AllArgsConstructor; +import org.springblade.core.sharding.ShardingDataSourceProvider; +import org.springblade.core.sharding.constant.ShardingConstant; +import org.springblade.core.sharding.props.ShardingProperties; +import org.springframework.beans.factory.SmartInitializingSingleton; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.*; +import org.springframework.core.annotation.Order; + +import javax.sql.DataSource; +import java.util.List; + +/** + * ShardingSphere与DynamicDatasource配置类 + * 此配置类用于未开启租户模块数据库隔离功能的场景 + * + * @author Chill + */ +@Configuration +@EnableConfigurationProperties({DynamicDataSourceProperties.class, ShardingProperties.class}) +@AutoConfiguration(before = {DruidDataSourceAutoConfigure.class, DynamicDataSourceAutoConfiguration.class}) +@Import(value = {DynamicDataSourceCreatorAutoConfiguration.class}) +@ConditionalOnProperty(value = ShardingProperties.PREFIX + ".enabled", havingValue = "true") +public class ShardingConfiguration { + + @Lazy + @Resource(name = "shardingSphereDataSource") + private DataSource shardingSphereDataSource; + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(value = ShardingConstant.TENANT_DYNAMIC_DATASOURCE_PROP, havingValue = "false") + public DataSourceInitEvent dataSourceInitEvent() { + return new EncDataSourceInitEvent(); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(value = ShardingConstant.TENANT_DYNAMIC_DATASOURCE_PROP, havingValue = "false") + public DefaultDataSourceCreator dataSourceCreator(List dataSourceCreators, DataSourceInitEvent dataSourceInitEvent, DynamicDataSourceProperties properties) { + DefaultDataSourceCreator creator = new DefaultDataSourceCreator(); + creator.setCreators(dataSourceCreators); + creator.setDataSourceInitEvent(dataSourceInitEvent); + creator.setPublicKey(properties.getPublicKey()); + creator.setLazy(properties.getLazy()); + creator.setP6spy(properties.getP6spy()); + creator.setSeata(properties.getSeata()); + creator.setSeataMode(properties.getSeataMode()); + return creator; + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(value = ShardingConstant.TENANT_DYNAMIC_DATASOURCE_PROP, havingValue = "false") + public DynamicDataSourceAnnotationInterceptor tenantDataSourceAnnotationInterceptor(DsProcessor dsProcessor, DynamicDataSourceProperties dynamicDataSourceProperties) { + return new DynamicDataSourceAnnotationInterceptor(dynamicDataSourceProperties.getAop().getAllowedPublicOnly(), dsProcessor); + } + + @Bean + @ConditionalOnMissingBean + @Role(value = BeanDefinition.ROLE_INFRASTRUCTURE) + @ConditionalOnProperty(value = ShardingConstant.TENANT_DYNAMIC_DATASOURCE_PROP, havingValue = "false") + public DynamicDataSourceAnnotationAdvisor dynamicDatasourceAnnotationAdvisor(DynamicDataSourceAnnotationInterceptor dynamicDataSourceAnnotationInterceptor, DynamicDataSourceProperties dynamicDataSourceProperties) { + DynamicDataSourceAnnotationAdvisor advisor = new DynamicDataSourceAnnotationAdvisor(dynamicDataSourceAnnotationInterceptor, DS.class); + advisor.setOrder(dynamicDataSourceProperties.getAop().getOrder()); + return advisor; + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(value = ShardingConstant.TENANT_DYNAMIC_DATASOURCE_PROP, havingValue = "false") + public DsProcessor dsProcessor() { + DsProcessor headerProcessor = new DsJakartaHeaderProcessor(); + DsProcessor sessionProcessor = new DsJakartaSessionProcessor(); + DsSpelExpressionProcessor spelExpressionProcessor = new DsSpelExpressionProcessor(); + headerProcessor.setNextProcessor(sessionProcessor); + sessionProcessor.setNextProcessor(spelExpressionProcessor); + return headerProcessor; + } + + /** + * 自定义分库分表动态数据源加载逻辑 + */ + @Bean + @Primary + @ConditionalOnProperty(value = ShardingConstant.TENANT_DYNAMIC_DATASOURCE_PROP, havingValue = "false") + public DynamicDataSourceProvider dynamicDataSourceProvider(DefaultDataSourceCreator dataSourceCreator, DataSourceProperties dataSourceProperties, DynamicDataSourceProperties dynamicDataSourceProperties) { + String driverClassName = dataSourceProperties.getDriverClassName(); + String url = dataSourceProperties.getUrl(); + String username = dataSourceProperties.getUsername(); + String password = dataSourceProperties.getPassword(); + DataSourceProperty master = dynamicDataSourceProperties.getDatasource().get(dynamicDataSourceProperties.getPrimary()); + if (master != null) { + driverClassName = master.getDriverClassName(); + url = master.getUrl(); + username = master.getUsername(); + password = master.getPassword(); + } + return new ShardingDataSourceProvider(dataSourceCreator, dynamicDataSourceProperties, driverClassName, url, username, password, shardingSphereDataSource); + } + + /** + * 配置分库分表动态数据源 + */ + @Bean + @Primary + @ConditionalOnProperty(value = ShardingConstant.TENANT_DYNAMIC_DATASOURCE_PROP, havingValue = "false") + public DataSource dataSource(List providers, DynamicDataSourceProperties dynamicDataSourceProperties) { + DynamicRoutingDataSource dataSource = new DynamicRoutingDataSource(providers); + dataSource.setPrimary(dynamicDataSourceProperties.getPrimary()); + dataSource.setStrict(dynamicDataSourceProperties.getStrict()); + dataSource.setStrategy(dynamicDataSourceProperties.getStrategy()); + dataSource.setP6spy(dynamicDataSourceProperties.getP6spy()); + dataSource.setSeata(dynamicDataSourceProperties.getSeata()); + return dataSource; + } + + @Order + @AutoConfiguration + @AllArgsConstructor + @ConditionalOnProperty(value = ShardingConstant.TENANT_DYNAMIC_DATASOURCE_PROP, havingValue = "true") + public static class ShardingSphereDataSourceConfiguration implements SmartInitializingSingleton { + @Lazy + @Resource(name = "shardingSphereDataSource") + private final DataSource shardingSphereDataSource; + + private final DataSource dataSource; + + @Override + public void afterSingletonsInstantiated() { + if (shardingSphereDataSource != null) { + // 获取储存的数据源集合 + DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource; + // 设置ShardingSphere数据源 + ds.addDataSource(ShardingConstant.SHARDING_DATASOURCE_KEY, shardingSphereDataSource); + } + + } + } + + +} diff --git a/blade-starter-sharding/src/main/java/org/springblade/core/sharding/constant/ShardingConstant.java b/blade-starter-sharding/src/main/java/org/springblade/core/sharding/constant/ShardingConstant.java new file mode 100644 index 0000000..43dc52c --- /dev/null +++ b/blade-starter-sharding/src/main/java/org/springblade/core/sharding/constant/ShardingConstant.java @@ -0,0 +1,44 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.sharding.constant; + +/** + * ShardingSphere常量 + * + * @author Chill + */ +public interface ShardingConstant { + /** + * sharding数据源缓存名 + */ + String SHARDING_DATASOURCE_KEY = "sharding"; + + /** + * 租户动态数据源键 + */ + String TENANT_DYNAMIC_DATASOURCE_PROP = "blade.tenant.dynamic-datasource"; + +} diff --git a/blade-starter-sharding/src/main/java/org/springblade/core/sharding/processor/ShardingEnvPostProcessor.java b/blade-starter-sharding/src/main/java/org/springblade/core/sharding/processor/ShardingEnvPostProcessor.java new file mode 100644 index 0000000..81fb5ca --- /dev/null +++ b/blade-starter-sharding/src/main/java/org/springblade/core/sharding/processor/ShardingEnvPostProcessor.java @@ -0,0 +1,57 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.sharding.processor; + +import org.springblade.core.auto.annotation.AutoEnvPostProcessor; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.env.EnvironmentPostProcessor; +import org.springframework.core.Ordered; +import org.springframework.core.env.ConfigurableEnvironment; + +/** + * 初始化分库分表配置 + * + * @author Chill + */ +@AutoEnvPostProcessor +public class ShardingEnvPostProcessor implements EnvironmentPostProcessor, Ordered { + + private static final String DYNAMIC_DATASOURCE_KEY = "spring.datasource.dynamic.enabled"; + + private static final String AUTOCONFIGURE_EXCLUDE_KEY = "spring.autoconfigure.exclude"; + + + @Override + public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { + environment.getSystemProperties().put(DYNAMIC_DATASOURCE_KEY, "false"); + environment.getSystemProperties().put(AUTOCONFIGURE_EXCLUDE_KEY, "com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceAutoConfigure"); + } + + @Override + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE; + } +} diff --git a/blade-starter-sharding/src/main/java/org/springblade/core/sharding/props/ShardingProperties.java b/blade-starter-sharding/src/main/java/org/springblade/core/sharding/props/ShardingProperties.java new file mode 100644 index 0000000..f16d770 --- /dev/null +++ b/blade-starter-sharding/src/main/java/org/springblade/core/sharding/props/ShardingProperties.java @@ -0,0 +1,52 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.sharding.props; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * 分库分表配置 + * + * @author Chill + */ +@Getter +@Setter +@ConfigurationProperties(ShardingProperties.PREFIX) +public class ShardingProperties { + /** + * 配置前缀 + */ + public static final String PREFIX = "blade.sharding"; + + /** + * 是否开启分库分表 + */ + private Boolean enabled = Boolean.FALSE; + + +} diff --git a/blade-starter-sms/pom.xml b/blade-starter-sms/pom.xml new file mode 100644 index 0000000..748df09 --- /dev/null +++ b/blade-starter-sms/pom.xml @@ -0,0 +1,58 @@ + + + 4.0.0 + + BladeX-Tool + org.springblade + ${revision} + + + blade-starter-sms + ${project.artifactId} + ${project.parent.version} + jar + + + + org.springblade + blade-core-tool + + + org.springblade + blade-starter-redis + + + jackson-databind + com.fasterxml.jackson.core + + + com.aliyun + aliyun-java-sdk-core + provided + + + com.qiniu + qiniu-java-sdk + provided + + + com.github.qcloudsms + qcloudsms + provided + + + com.yunpian.sdk + yunpian-java-sdk + provided + + + org.springblade + blade-core-auto + provided + + + + + diff --git a/blade-starter-sms/src/main/java/org/springblade/core/sms/AliSmsTemplate.java b/blade-starter-sms/src/main/java/org/springblade/core/sms/AliSmsTemplate.java new file mode 100644 index 0000000..586cce5 --- /dev/null +++ b/blade-starter-sms/src/main/java/org/springblade/core/sms/AliSmsTemplate.java @@ -0,0 +1,119 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.sms; + +import com.aliyuncs.CommonRequest; +import com.aliyuncs.CommonResponse; +import com.aliyuncs.IAcsClient; +import com.aliyuncs.exceptions.ClientException; +import com.aliyuncs.http.MethodType; +import lombok.AllArgsConstructor; +import org.springblade.core.redis.cache.BladeRedis; +import org.springblade.core.sms.model.SmsCode; +import org.springblade.core.sms.model.SmsData; +import org.springblade.core.sms.model.SmsResponse; +import org.springblade.core.sms.props.SmsProperties; +import org.springblade.core.tool.jackson.JsonUtil; +import org.springblade.core.tool.utils.StringUtil; +import org.springframework.http.HttpStatus; + +import java.time.Duration; +import java.util.Collection; +import java.util.Map; + +/** + * 阿里云短信发送类 + * + * @author Chill + */ +@AllArgsConstructor +public class AliSmsTemplate implements SmsTemplate { + + private static final int SUCCESS = 200; + private static final String FAIL = "fail"; + private static final String OK = "ok"; + private static final String DOMAIN = "dysmsapi.aliyuncs.com"; + private static final String VERSION = "2017-05-25"; + private static final String ACTION = "SendSms"; + + private final SmsProperties smsProperties; + private final IAcsClient acsClient; + private final BladeRedis bladeRedis; + + @Override + public SmsResponse sendMessage(SmsData smsData, Collection phones) { + CommonRequest request = new CommonRequest(); + request.setSysMethod(MethodType.POST); + request.setSysDomain(DOMAIN); + request.setSysVersion(VERSION); + request.setSysAction(ACTION); + request.putQueryParameter("PhoneNumbers", StringUtil.join(phones)); + request.putQueryParameter("TemplateCode", smsProperties.getTemplateId()); + request.putQueryParameter("TemplateParam", JsonUtil.toJson(smsData.getParams())); + request.putQueryParameter("SignName", smsProperties.getSignName()); + try { + CommonResponse response = acsClient.getCommonResponse(request); + Map data = JsonUtil.toMap(response.getData()); + String code = FAIL; + if (data != null) { + code = String.valueOf(data.get("Code")); + } + return new SmsResponse(response.getHttpStatus() == SUCCESS && code.equalsIgnoreCase(OK), response.getHttpStatus(), response.getData()); + } catch (ClientException e) { + e.printStackTrace(); + return new SmsResponse(Boolean.FALSE, HttpStatus.INTERNAL_SERVER_ERROR.value(), e.getMessage()); + } + } + + @Override + public SmsCode sendValidate(SmsData smsData, String phone) { + SmsCode smsCode = new SmsCode(); + boolean temp = sendSingle(smsData, phone); + if (temp && StringUtil.isNotBlank(smsData.getKey())) { + String id = StringUtil.randomUUID(); + String value = smsData.getParams().get(smsData.getKey()); + bladeRedis.setEx(cacheKey(phone, id), value, Duration.ofMinutes(30)); + smsCode.setId(id).setValue(value); + } else { + smsCode.setSuccess(Boolean.FALSE); + } + return smsCode; + } + + @Override + public boolean validateMessage(SmsCode smsCode) { + String id = smsCode.getId(); + String value = smsCode.getValue(); + String phone = smsCode.getPhone(); + String cache = bladeRedis.get(cacheKey(phone, id)); + if (StringUtil.isNotBlank(value) && StringUtil.equalsIgnoreCase(cache, value)) { + bladeRedis.del(cacheKey(phone, id)); + return true; + } + return false; + } + +} diff --git a/blade-starter-sms/src/main/java/org/springblade/core/sms/QiniuSmsTemplate.java b/blade-starter-sms/src/main/java/org/springblade/core/sms/QiniuSmsTemplate.java new file mode 100644 index 0000000..b0d1435 --- /dev/null +++ b/blade-starter-sms/src/main/java/org/springblade/core/sms/QiniuSmsTemplate.java @@ -0,0 +1,94 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.sms; + +import com.qiniu.common.QiniuException; +import com.qiniu.http.Response; +import com.qiniu.sms.SmsManager; +import lombok.AllArgsConstructor; +import org.springblade.core.redis.cache.BladeRedis; +import org.springblade.core.sms.model.SmsCode; +import org.springblade.core.sms.model.SmsData; +import org.springblade.core.sms.model.SmsResponse; +import org.springblade.core.sms.props.SmsProperties; +import org.springblade.core.tool.utils.StringUtil; +import org.springframework.http.HttpStatus; + +import java.time.Duration; +import java.util.Collection; + +/** + * 七牛云短信发送类 + * + * @author Chill + */ +@AllArgsConstructor +public class QiniuSmsTemplate implements SmsTemplate { + + private final SmsProperties smsProperties; + private final SmsManager smsManager; + private final BladeRedis bladeRedis; + + @Override + public SmsResponse sendMessage(SmsData smsData, Collection phones) { + try { + Response response = smsManager.sendMessage(smsProperties.getTemplateId(), StringUtil.toStringArray(phones), smsData.getParams()); + return new SmsResponse(response.isOK(), response.statusCode, response.toString()); + } catch (QiniuException e) { + e.printStackTrace(); + return new SmsResponse(Boolean.FALSE, HttpStatus.INTERNAL_SERVER_ERROR.value(), e.getMessage()); + } + } + + @Override + public SmsCode sendValidate(SmsData smsData, String phone) { + SmsCode smsCode = new SmsCode(); + boolean temp = sendSingle(smsData, phone); + if (temp && StringUtil.isNotBlank(smsData.getKey())) { + String id = StringUtil.randomUUID(); + String value = smsData.getParams().get(smsData.getKey()); + bladeRedis.setEx(cacheKey(phone, id), value, Duration.ofMinutes(30)); + smsCode.setId(id).setValue(value); + } else { + smsCode.setSuccess(Boolean.FALSE); + } + return smsCode; + } + + @Override + public boolean validateMessage(SmsCode smsCode) { + String id = smsCode.getId(); + String value = smsCode.getValue(); + String phone = smsCode.getPhone(); + String cache = bladeRedis.get(cacheKey(phone, id)); + if (StringUtil.isNotBlank(value) && StringUtil.equalsIgnoreCase(cache, value)) { + bladeRedis.del(cacheKey(phone, id)); + return true; + } + return false; + } + +} diff --git a/blade-starter-sms/src/main/java/org/springblade/core/sms/SmsTemplate.java b/blade-starter-sms/src/main/java/org/springblade/core/sms/SmsTemplate.java new file mode 100644 index 0000000..4e8c12d --- /dev/null +++ b/blade-starter-sms/src/main/java/org/springblade/core/sms/SmsTemplate.java @@ -0,0 +1,120 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.sms; + +import org.springblade.core.sms.model.SmsCode; +import org.springblade.core.sms.model.SmsData; +import org.springblade.core.sms.model.SmsInfo; +import org.springblade.core.sms.model.SmsResponse; +import org.springblade.core.tool.utils.StringPool; +import org.springframework.util.StringUtils; + +import java.util.Collection; +import java.util.Collections; + +import static org.springblade.core.sms.constant.SmsConstant.CAPTCHA_KEY; + +/** + * 短信通用封装 + * + * @author Chill + */ +public interface SmsTemplate { + + /** + * 缓存键值 + * + * @param phone 手机号 + * @param id 键值 + * @return 缓存键值返回 + */ + default String cacheKey(String phone, String id) { + return CAPTCHA_KEY + phone + StringPool.COLON + id; + } + + /** + * 发送短信 + * + * @param smsInfo 短信信息 + * @return 发送返回 + */ + default boolean send(SmsInfo smsInfo) { + return sendMulti(smsInfo.getSmsData(), smsInfo.getPhones()); + } + + /** + * 发送短信 + * + * @param smsData 短信内容 + * @param phone 手机号 + * @return 发送返回 + */ + default boolean sendSingle(SmsData smsData, String phone) { + if (StringUtils.isEmpty(phone)) { + return Boolean.FALSE; + } + return sendMulti(smsData, Collections.singletonList(phone)); + } + + /** + * 发送短信 + * + * @param smsData 短信内容 + * @param phones 手机号列表 + * @return 发送返回 + */ + default boolean sendMulti(SmsData smsData, Collection phones) { + SmsResponse response = sendMessage(smsData, phones); + return response.isSuccess(); + } + + /** + * 发送短信 + * + * @param smsData 短信内容 + * @param phones 手机号列表 + * @return 发送返回 + */ + SmsResponse sendMessage(SmsData smsData, Collection phones); + + /** + * 发送验证码 + * + * @param smsData 短信内容 + * @param phone 手机号 + * @return 发送返回 + */ + SmsCode sendValidate(SmsData smsData, String phone); + + /** + * 校验验证码 + * + * @param smsCode 验证码内容 + * @return 是否校验成功 + */ + boolean validateMessage(SmsCode smsCode); + +} diff --git a/blade-starter-sms/src/main/java/org/springblade/core/sms/TencentSmsTemplate.java b/blade-starter-sms/src/main/java/org/springblade/core/sms/TencentSmsTemplate.java new file mode 100644 index 0000000..f98cf22 --- /dev/null +++ b/blade-starter-sms/src/main/java/org/springblade/core/sms/TencentSmsTemplate.java @@ -0,0 +1,110 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.sms; + +import com.github.qcloudsms.SmsMultiSender; +import com.github.qcloudsms.SmsMultiSenderResult; +import com.github.qcloudsms.httpclient.HTTPException; +import lombok.AllArgsConstructor; +import org.springblade.core.redis.cache.BladeRedis; +import org.springblade.core.sms.model.SmsCode; +import org.springblade.core.sms.model.SmsData; +import org.springblade.core.sms.model.SmsResponse; +import org.springblade.core.sms.props.SmsProperties; +import org.springblade.core.tool.utils.Func; +import org.springblade.core.tool.utils.StringPool; +import org.springblade.core.tool.utils.StringUtil; +import org.springframework.http.HttpStatus; + +import java.io.IOException; +import java.time.Duration; +import java.util.Collection; + +/** + * 腾讯云短信发送类 + * + * @author Chill + */ +@AllArgsConstructor +public class TencentSmsTemplate implements SmsTemplate { + + private static final int SUCCESS = 0; + private static final String NATION_CODE = "86"; + + private final SmsProperties smsProperties; + private final SmsMultiSender smsSender; + private final BladeRedis bladeRedis; + + + @Override + public SmsResponse sendMessage(SmsData smsData, Collection phones) { + try { + Collection values = smsData.getParams().values(); + String[] params = StringUtil.toStringArray(values); + SmsMultiSenderResult senderResult = smsSender.sendWithParam( + NATION_CODE, + StringUtil.toStringArray(phones), + Func.toInt(smsProperties.getTemplateId()), + params, + smsProperties.getSignName(), + StringPool.EMPTY, StringPool.EMPTY + ); + return new SmsResponse(senderResult.result == SUCCESS, senderResult.result, senderResult.toString()); + } catch (HTTPException | IOException e) { + e.printStackTrace(); + return new SmsResponse(Boolean.FALSE, HttpStatus.INTERNAL_SERVER_ERROR.value(), e.getMessage()); + } + } + + @Override + public SmsCode sendValidate(SmsData smsData, String phone) { + SmsCode smsCode = new SmsCode(); + boolean temp = sendSingle(smsData, phone); + if (temp && StringUtil.isNotBlank(smsData.getKey())) { + String id = StringUtil.randomUUID(); + String value = smsData.getParams().get(smsData.getKey()); + bladeRedis.setEx(cacheKey(phone, id), value, Duration.ofMinutes(30)); + smsCode.setId(id).setValue(value); + } else { + smsCode.setSuccess(Boolean.FALSE); + } + return smsCode; + } + + @Override + public boolean validateMessage(SmsCode smsCode) { + String id = smsCode.getId(); + String value = smsCode.getValue(); + String phone = smsCode.getPhone(); + String cache = bladeRedis.get(cacheKey(phone, id)); + if (StringUtil.isNotBlank(value) && StringUtil.equalsIgnoreCase(cache, value)) { + bladeRedis.del(cacheKey(phone, id)); + return true; + } + return false; + } + +} diff --git a/blade-starter-sms/src/main/java/org/springblade/core/sms/YunpianSmsTemplate.java b/blade-starter-sms/src/main/java/org/springblade/core/sms/YunpianSmsTemplate.java new file mode 100644 index 0000000..3df383d --- /dev/null +++ b/blade-starter-sms/src/main/java/org/springblade/core/sms/YunpianSmsTemplate.java @@ -0,0 +1,100 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.sms; + +import com.yunpian.sdk.YunpianClient; +import com.yunpian.sdk.constant.Code; +import com.yunpian.sdk.model.Result; +import com.yunpian.sdk.model.SmsBatchSend; +import lombok.AllArgsConstructor; +import org.springblade.core.redis.cache.BladeRedis; +import org.springblade.core.sms.model.SmsCode; +import org.springblade.core.sms.model.SmsData; +import org.springblade.core.sms.model.SmsResponse; +import org.springblade.core.sms.props.SmsProperties; +import org.springblade.core.tool.support.Kv; +import org.springblade.core.tool.utils.PlaceholderUtil; +import org.springblade.core.tool.utils.StringPool; +import org.springblade.core.tool.utils.StringUtil; + +import java.time.Duration; +import java.util.Collection; +import java.util.Map; + +/** + * 云片短信发送类 + * + * @author Chill + */ +@AllArgsConstructor +public class YunpianSmsTemplate implements SmsTemplate { + + private final SmsProperties smsProperties; + private final YunpianClient client; + private final BladeRedis bladeRedis; + + @Override + public SmsResponse sendMessage(SmsData smsData, Collection phones) { + String templateId = smsProperties.getTemplateId(); + // 云片短信模板内容替换, 占位符格式为官方默认的 #code# + String templateText = PlaceholderUtil.getResolver(StringPool.HASH, StringPool.HASH).resolveByMap( + templateId, Kv.create().setAll(smsData.getParams()) + ); + Map param = client.newParam(2); + param.put(YunpianClient.MOBILE, StringUtil.join(phones)); + param.put(YunpianClient.TEXT, templateText); + Result result = client.sms().multi_send(param); + return new SmsResponse(result.getCode() == Code.OK, result.getCode(), result.toString()); + } + + @Override + public SmsCode sendValidate(SmsData smsData, String phone) { + SmsCode smsCode = new SmsCode(); + boolean temp = sendSingle(smsData, phone); + if (temp && StringUtil.isNotBlank(smsData.getKey())) { + String id = StringUtil.randomUUID(); + String value = smsData.getParams().get(smsData.getKey()); + bladeRedis.setEx(cacheKey(phone, id), value, Duration.ofMinutes(30)); + smsCode.setId(id).setValue(value); + } else { + smsCode.setSuccess(Boolean.FALSE); + } + return smsCode; + } + + @Override + public boolean validateMessage(SmsCode smsCode) { + String id = smsCode.getId(); + String value = smsCode.getValue(); + String phone = smsCode.getPhone(); + String cache = bladeRedis.get(cacheKey(phone, id)); + if (StringUtil.isNotBlank(value) && StringUtil.equalsIgnoreCase(cache, value)) { + bladeRedis.del(cacheKey(phone, id)); + return true; + } + return false; + } +} diff --git a/blade-starter-sms/src/main/java/org/springblade/core/sms/config/AliSmsConfiguration.java b/blade-starter-sms/src/main/java/org/springblade/core/sms/config/AliSmsConfiguration.java new file mode 100644 index 0000000..9a16a21 --- /dev/null +++ b/blade-starter-sms/src/main/java/org/springblade/core/sms/config/AliSmsConfiguration.java @@ -0,0 +1,63 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.sms.config; + +import com.aliyuncs.DefaultAcsClient; +import com.aliyuncs.IAcsClient; +import com.aliyuncs.profile.DefaultProfile; +import com.aliyuncs.profile.IClientProfile; +import lombok.AllArgsConstructor; +import org.springblade.core.redis.cache.BladeRedis; +import org.springblade.core.sms.AliSmsTemplate; +import org.springblade.core.sms.props.SmsProperties; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; + +/** + * 阿里云短信配置类 + * + * @author Chill + */ +@AutoConfiguration +@AllArgsConstructor +@ConditionalOnClass(IAcsClient.class) +@EnableConfigurationProperties(SmsProperties.class) +@ConditionalOnProperty(value = "sms.name", havingValue = "aliyun") +public class AliSmsConfiguration { + + private final BladeRedis bladeRedis; + + @Bean + public AliSmsTemplate aliSmsTemplate(SmsProperties smsProperties) { + IClientProfile profile = DefaultProfile.getProfile(smsProperties.getRegionId(), smsProperties.getAccessKey(), smsProperties.getSecretKey()); + IAcsClient acsClient = new DefaultAcsClient(profile); + return new AliSmsTemplate(smsProperties, acsClient, bladeRedis); + } + +} diff --git a/blade-starter-sms/src/main/java/org/springblade/core/sms/config/QiniuSmsConfiguration.java b/blade-starter-sms/src/main/java/org/springblade/core/sms/config/QiniuSmsConfiguration.java new file mode 100644 index 0000000..1bf0145 --- /dev/null +++ b/blade-starter-sms/src/main/java/org/springblade/core/sms/config/QiniuSmsConfiguration.java @@ -0,0 +1,61 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.sms.config; + +import com.qiniu.sms.SmsManager; +import com.qiniu.util.Auth; +import lombok.AllArgsConstructor; +import org.springblade.core.redis.cache.BladeRedis; +import org.springblade.core.sms.QiniuSmsTemplate; +import org.springblade.core.sms.props.SmsProperties; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; + +/** + * 阿里云短信配置类 + * + * @author Chill + */ +@AutoConfiguration +@AllArgsConstructor +@ConditionalOnClass(SmsManager.class) +@EnableConfigurationProperties(SmsProperties.class) +@ConditionalOnProperty(value = "sms.name", havingValue = "qiniu") +public class QiniuSmsConfiguration { + + private final BladeRedis bladeRedis; + + @Bean + public QiniuSmsTemplate qiniuSmsTemplate(SmsProperties smsProperties) { + Auth auth = Auth.create(smsProperties.getAccessKey(), smsProperties.getSecretKey()); + SmsManager smsManager = new SmsManager(auth); + return new QiniuSmsTemplate(smsProperties, smsManager, bladeRedis); + } + +} diff --git a/blade-starter-sms/src/main/java/org/springblade/core/sms/config/SmsConfiguration.java b/blade-starter-sms/src/main/java/org/springblade/core/sms/config/SmsConfiguration.java new file mode 100644 index 0000000..ac4bb90 --- /dev/null +++ b/blade-starter-sms/src/main/java/org/springblade/core/sms/config/SmsConfiguration.java @@ -0,0 +1,42 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.sms.config; + +import lombok.AllArgsConstructor; +import org.springblade.core.sms.props.SmsProperties; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; + +/** + * Sms配置类 + * + * @author Chill + */ +@AutoConfiguration +@AllArgsConstructor +@EnableConfigurationProperties(SmsProperties.class) +public class SmsConfiguration { +} diff --git a/blade-starter-sms/src/main/java/org/springblade/core/sms/config/TencentSmsConfiguration.java b/blade-starter-sms/src/main/java/org/springblade/core/sms/config/TencentSmsConfiguration.java new file mode 100644 index 0000000..ea23b66 --- /dev/null +++ b/blade-starter-sms/src/main/java/org/springblade/core/sms/config/TencentSmsConfiguration.java @@ -0,0 +1,60 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.sms.config; + +import com.github.qcloudsms.SmsMultiSender; +import lombok.AllArgsConstructor; +import org.springblade.core.redis.cache.BladeRedis; +import org.springblade.core.sms.TencentSmsTemplate; +import org.springblade.core.sms.props.SmsProperties; +import org.springblade.core.tool.utils.Func; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; + +/** + * 腾讯云短信配置类 + * + * @author Chill + */ +@AutoConfiguration +@AllArgsConstructor +@ConditionalOnClass(SmsMultiSender.class) +@EnableConfigurationProperties(SmsProperties.class) +@ConditionalOnProperty(value = "sms.name", havingValue = "tencent") +public class TencentSmsConfiguration { + + private final BladeRedis bladeRedis; + + @Bean + public TencentSmsTemplate tencentSmsTemplate(SmsProperties smsProperties) { + SmsMultiSender smsSender = new SmsMultiSender(Func.toInt(smsProperties.getAccessKey()), smsProperties.getSecretKey()); + return new TencentSmsTemplate(smsProperties, smsSender, bladeRedis); + } + +} diff --git a/blade-starter-sms/src/main/java/org/springblade/core/sms/config/YunpianSmsConfiguration.java b/blade-starter-sms/src/main/java/org/springblade/core/sms/config/YunpianSmsConfiguration.java new file mode 100644 index 0000000..3badcba --- /dev/null +++ b/blade-starter-sms/src/main/java/org/springblade/core/sms/config/YunpianSmsConfiguration.java @@ -0,0 +1,59 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.sms.config; + +import com.yunpian.sdk.YunpianClient; +import lombok.AllArgsConstructor; +import org.springblade.core.redis.cache.BladeRedis; +import org.springblade.core.sms.YunpianSmsTemplate; +import org.springblade.core.sms.props.SmsProperties; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; + +/** + * 云片短信配置类 + * + * @author Chill + */ +@AutoConfiguration +@AllArgsConstructor +@ConditionalOnClass(YunpianClient.class) +@EnableConfigurationProperties(SmsProperties.class) +@ConditionalOnProperty(value = "sms.name", havingValue = "yunpian") +public class YunpianSmsConfiguration { + + private final BladeRedis bladeRedis; + + @Bean + public YunpianSmsTemplate yunpianSmsTemplate(SmsProperties smsProperties) { + YunpianClient client = new YunpianClient(smsProperties.getAccessKey()).init(); + return new YunpianSmsTemplate(smsProperties, client, bladeRedis); + } + +} diff --git a/blade-starter-sms/src/main/java/org/springblade/core/sms/constant/SmsConstant.java b/blade-starter-sms/src/main/java/org/springblade/core/sms/constant/SmsConstant.java new file mode 100644 index 0000000..ca02c36 --- /dev/null +++ b/blade-starter-sms/src/main/java/org/springblade/core/sms/constant/SmsConstant.java @@ -0,0 +1,40 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.sms.constant; + +/** + * 短信服务常量 + * + * @author Chill + */ +public interface SmsConstant { + + /** + * 通用缓存key + */ + String CAPTCHA_KEY = "blade:sms::captcha:"; + +} diff --git a/blade-starter-sms/src/main/java/org/springblade/core/sms/enums/SmsEnum.java b/blade-starter-sms/src/main/java/org/springblade/core/sms/enums/SmsEnum.java new file mode 100644 index 0000000..f0e3958 --- /dev/null +++ b/blade-starter-sms/src/main/java/org/springblade/core/sms/enums/SmsEnum.java @@ -0,0 +1,89 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.sms.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * Sms枚举类 + * + * @author Chill + */ +@Getter +@AllArgsConstructor +public enum SmsEnum { + + /** + * yunpian + */ + YUNPIAN("yunpian", 1), + + /** + * qiniu + */ + QINIU("qiniu", 2), + + /** + * ali + */ + ALI("ali", 3), + + /** + * tencent + */ + TENCENT("tencent", 4), + ; + + /** + * 名称 + */ + final String name; + /** + * 类型 + */ + final int category; + + /** + * 匹配枚举值 + * + * @param name 名称 + * @return OssEnum + */ + public static SmsEnum of(String name) { + if (name == null) { + return null; + } + SmsEnum[] values = SmsEnum.values(); + for (SmsEnum smsEnum : values) { + if (smsEnum.name.equals(name)) { + return smsEnum; + } + } + return null; + } + +} diff --git a/blade-starter-sms/src/main/java/org/springblade/core/sms/enums/SmsStatusEnum.java b/blade-starter-sms/src/main/java/org/springblade/core/sms/enums/SmsStatusEnum.java new file mode 100644 index 0000000..f326900 --- /dev/null +++ b/blade-starter-sms/src/main/java/org/springblade/core/sms/enums/SmsStatusEnum.java @@ -0,0 +1,55 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.sms.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * Sms类型枚举 + * + * @author Chill + */ +@Getter +@AllArgsConstructor +public enum SmsStatusEnum { + + /** + * 关闭 + */ + DISABLE(1), + /** + * 启用 + */ + ENABLE(2), + ; + + /** + * 类型编号 + */ + final int num; + +} diff --git a/blade-starter-sms/src/main/java/org/springblade/core/sms/model/SmsCode.java b/blade-starter-sms/src/main/java/org/springblade/core/sms/model/SmsCode.java new file mode 100644 index 0000000..088a1dc --- /dev/null +++ b/blade-starter-sms/src/main/java/org/springblade/core/sms/model/SmsCode.java @@ -0,0 +1,68 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.sms.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 校验信息 + * + * @author Chill + */ +@Data +@Accessors(chain = true) +public class SmsCode implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 是否成功 + */ + private boolean success = Boolean.TRUE; + + /** + * 变量phone,用于redis进行比对 + */ + private String phone; + + /** + * 变量id,用于redis进行比对 + */ + private String id; + + /** + * 变量值,用于redis进行比对 + */ + @JsonIgnore + private String value; + +} diff --git a/blade-starter-sms/src/main/java/org/springblade/core/sms/model/SmsData.java b/blade-starter-sms/src/main/java/org/springblade/core/sms/model/SmsData.java new file mode 100644 index 0000000..8426673 --- /dev/null +++ b/blade-starter-sms/src/main/java/org/springblade/core/sms/model/SmsData.java @@ -0,0 +1,66 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.sms.model; + +import lombok.Data; +import lombok.experimental.Accessors; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Map; + +/** + * 通知内容 + * + * @author Chill + */ +@Data +@Accessors(chain = true) +public class SmsData implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 构造器 + * + * @param params 参数列表 + */ + public SmsData(Map params) { + this.params = params; + } + + /** + * 变量key,用于从参数列表获取变量值 + */ + private String key; + + /** + * 参数列表 + */ + private Map params; + +} diff --git a/blade-starter-sms/src/main/java/org/springblade/core/sms/model/SmsInfo.java b/blade-starter-sms/src/main/java/org/springblade/core/sms/model/SmsInfo.java new file mode 100644 index 0000000..3d6f6a4 --- /dev/null +++ b/blade-starter-sms/src/main/java/org/springblade/core/sms/model/SmsInfo.java @@ -0,0 +1,56 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.sms.model; + +import lombok.Data; +import lombok.experimental.Accessors; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Collection; + +/** + * 通知信息 + * + * @author Chill + */ +@Data +@Accessors(chain = true) +public class SmsInfo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 通知内容 + */ + private SmsData smsData; + + /** + * 号码列表 + */ + private Collection phones; +} diff --git a/blade-starter-sms/src/main/java/org/springblade/core/sms/model/SmsResponse.java b/blade-starter-sms/src/main/java/org/springblade/core/sms/model/SmsResponse.java new file mode 100644 index 0000000..38821a5 --- /dev/null +++ b/blade-starter-sms/src/main/java/org/springblade/core/sms/model/SmsResponse.java @@ -0,0 +1,61 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.sms.model; + +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 短信返回集合 + * + * @author Chill + */ +@Data +@AllArgsConstructor +public class SmsResponse implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 是否成功 + */ + private boolean success; + + /** + * 状态码 + */ + private Integer code; + + /** + * 返回消息 + */ + private String msg; + +} diff --git a/blade-starter-sms/src/main/java/org/springblade/core/sms/props/SmsProperties.java b/blade-starter-sms/src/main/java/org/springblade/core/sms/props/SmsProperties.java new file mode 100644 index 0000000..19d9331 --- /dev/null +++ b/blade-starter-sms/src/main/java/org/springblade/core/sms/props/SmsProperties.java @@ -0,0 +1,75 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.sms.props; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * 云短信配置 + * + * @author Chill + */ +@Data +@ConfigurationProperties(prefix = "sms") +public class SmsProperties { + + /** + * 是否启用 + */ + private Boolean enabled; + + /** + * 短信服务名称 + */ + private String name; + + /** + * 短信模板ID + */ + private String templateId; + + /** + * regionId + */ + private String regionId = "cn-hangzhou"; + + /** + * accessKey + */ + private String accessKey; + + /** + * secretKey + */ + private String secretKey; + + /** + * 短信签名 + */ + private String signName; + +} diff --git a/blade-starter-social/pom.xml b/blade-starter-social/pom.xml new file mode 100644 index 0000000..e3df0d2 --- /dev/null +++ b/blade-starter-social/pom.xml @@ -0,0 +1,40 @@ + + + 4.0.0 + + BladeX-Tool + org.springblade + ${revision} + + + blade-starter-social + ${project.artifactId} + ${project.parent.version} + jar + + + + + org.springblade + blade-starter-redis + + + + me.zhyd.oauth + JustAuth + + + org.apache.httpcomponents + httpclient + + + + org.springblade + blade-core-auto + provided + + + + diff --git a/blade-starter-social/src/main/java/org/springblade/core/social/cache/AuthStateRedisCache.java b/blade-starter-social/src/main/java/org/springblade/core/social/cache/AuthStateRedisCache.java new file mode 100644 index 0000000..82abc72 --- /dev/null +++ b/blade-starter-social/src/main/java/org/springblade/core/social/cache/AuthStateRedisCache.java @@ -0,0 +1,69 @@ +package org.springblade.core.social.cache; + +import lombok.AllArgsConstructor; +import me.zhyd.oauth.cache.AuthCacheConfig; +import me.zhyd.oauth.cache.AuthStateCache; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; + +import java.util.concurrent.TimeUnit; + +/** + * 扩展Redis版的state缓存 + * + * @author yadong.zhang, Chill + */ +@AllArgsConstructor +public class AuthStateRedisCache implements AuthStateCache { + + private final RedisTemplate redisTemplate; + + private final ValueOperations valueOperations; + + + /** + * 存入缓存,默认3分钟 + * + * @param key 缓存key + * @param value 缓存内容 + */ + @Override + public void cache(String key, String value) { + valueOperations.set(key, value, AuthCacheConfig.timeout, TimeUnit.MILLISECONDS); + } + + /** + * 存入缓存 + * + * @param key 缓存key + * @param value 缓存内容 + * @param timeout 指定缓存过期时间(毫秒) + */ + @Override + public void cache(String key, String value, long timeout) { + valueOperations.set(key, value, timeout, TimeUnit.MILLISECONDS); + } + + /** + * 获取缓存内容 + * + * @param key 缓存key + * @return 缓存内容 + */ + @Override + public String get(String key) { + return String.valueOf(valueOperations.get(key)); + } + + /** + * 是否存在key,如果对应key的value值已过期,也返回false + * + * @param key 缓存key + * @return true:存在key,并且value没过期;false:key不存在或者已过期 + */ + @Override + public boolean containsKey(String key) { + return redisTemplate.hasKey(key); + } + +} diff --git a/blade-starter-social/src/main/java/org/springblade/core/social/config/SocialConfiguration.java b/blade-starter-social/src/main/java/org/springblade/core/social/config/SocialConfiguration.java new file mode 100644 index 0000000..02890fd --- /dev/null +++ b/blade-starter-social/src/main/java/org/springblade/core/social/config/SocialConfiguration.java @@ -0,0 +1,66 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.social.config; + +import com.xkcoding.http.HttpUtil; +import com.xkcoding.http.support.Http; +import com.xkcoding.http.support.httpclient.HttpClientImpl; +import me.zhyd.oauth.cache.AuthStateCache; +import org.springblade.core.launch.props.BladePropertySource; +import org.springblade.core.redis.config.RedisTemplateConfiguration; +import org.springblade.core.social.cache.AuthStateRedisCache; +import org.springblade.core.social.props.SocialProperties; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.data.redis.core.RedisTemplate; + +/** + * SocialConfiguration + * + * @author Chill + */ +@EnableConfigurationProperties(SocialProperties.class) +@AutoConfiguration(after = RedisTemplateConfiguration.class) +@BladePropertySource(value = "classpath:/blade-social.yml") +public class SocialConfiguration { + + @Bean + @ConditionalOnMissingBean(Http.class) + public Http simpleHttp() { + HttpClientImpl httpClient = new HttpClientImpl(); + HttpUtil.setHttp(httpClient); + return httpClient; + } + + @Bean + @ConditionalOnMissingBean(AuthStateCache.class) + public AuthStateCache authStateCache(RedisTemplate redisTemplate) { + return new AuthStateRedisCache(redisTemplate, redisTemplate.opsForValue()); + } + +} diff --git a/blade-starter-social/src/main/java/org/springblade/core/social/props/SocialProperties.java b/blade-starter-social/src/main/java/org/springblade/core/social/props/SocialProperties.java new file mode 100644 index 0000000..a95b3d9 --- /dev/null +++ b/blade-starter-social/src/main/java/org/springblade/core/social/props/SocialProperties.java @@ -0,0 +1,67 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.social.props; + +import com.google.common.collect.Maps; +import lombok.Getter; +import lombok.Setter; +import me.zhyd.oauth.config.AuthConfig; +import me.zhyd.oauth.config.AuthDefaultSource; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.Map; + +/** + * SocialProperties + * + * @author Chill + */ +@Getter +@Setter +@ConfigurationProperties(prefix = "social") +public class SocialProperties { + + /** + * 启用 + */ + private Boolean enabled = false; + + /** + * 域名地址 + */ + private String domain; + + /** + * 类型 + */ + private Map oauth = Maps.newHashMap(); + + /** + * 别名 + */ + private Map alias = Maps.newHashMap(); + +} diff --git a/blade-starter-social/src/main/java/org/springblade/core/social/utils/SocialUtil.java b/blade-starter-social/src/main/java/org/springblade/core/social/utils/SocialUtil.java new file mode 100644 index 0000000..1cd1084 --- /dev/null +++ b/blade-starter-social/src/main/java/org/springblade/core/social/utils/SocialUtil.java @@ -0,0 +1,177 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.social.utils; + +import me.zhyd.oauth.cache.AuthStateCache; +import me.zhyd.oauth.config.AuthConfig; +import me.zhyd.oauth.config.AuthDefaultSource; +import me.zhyd.oauth.exception.AuthException; +import me.zhyd.oauth.request.*; +import org.springblade.core.social.props.SocialProperties; +import org.springblade.core.tool.utils.SpringUtil; + +import java.util.Objects; + +/** + * SocialUtil + * + * @author Chill + */ +public class SocialUtil { + + /** + * 自定义state缓存 + */ + private static AuthStateCache authStateCache; + + public static AuthStateCache getAuthStateCache() { + if (authStateCache == null) { + authStateCache = SpringUtil.getBean(AuthStateCache.class); + } + return authStateCache; + } + + /** + * 根据具体的授权来源,获取授权请求工具类 + * + * @param source 授权来源 + * @return AuthRequest + */ + public static AuthRequest getAuthRequest(String source, SocialProperties socialProperties) { + AuthDefaultSource authSource = Objects.requireNonNull(AuthDefaultSource.valueOf(source.toUpperCase())); + AuthConfig authConfig = socialProperties.getOauth().get(authSource); + if (authConfig == null) { + throw new AuthException("未获取到有效的Auth配置"); + } + AuthRequest authRequest = null; + switch (authSource) { + case GITHUB: + authRequest = new AuthGithubRequest(authConfig, getAuthStateCache()); + break; + case GITEE: + authRequest = new AuthGiteeRequest(authConfig, getAuthStateCache()); + break; + case OSCHINA: + authRequest = new AuthOschinaRequest(authConfig, getAuthStateCache()); + break; + case QQ: + authRequest = new AuthQqRequest(authConfig, getAuthStateCache()); + break; + case WECHAT_OPEN: + authRequest = new AuthWeChatOpenRequest(authConfig, getAuthStateCache()); + break; + case WECHAT_ENTERPRISE: + authRequest = new AuthWeChatEnterpriseQrcodeRequest(authConfig, getAuthStateCache()); + break; + case WECHAT_ENTERPRISE_WEB: + authRequest = new AuthWeChatEnterpriseWebRequest(authConfig, getAuthStateCache()); + break; + case WECHAT_MP: + authRequest = new AuthWeChatMpRequest(authConfig, getAuthStateCache()); + break; + case DINGTALK: + authRequest = new AuthDingTalkRequest(authConfig, getAuthStateCache()); + break; + case ALIPAY: + // 支付宝在创建回调地址时,不允许使用localhost或者127.0.0.1,所以这儿的回调地址使用的局域网内的ip + authRequest = new AuthAlipayRequest(authConfig, getAuthStateCache()); + break; + case BAIDU: + authRequest = new AuthBaiduRequest(authConfig, getAuthStateCache()); + break; + case WEIBO: + authRequest = new AuthWeiboRequest(authConfig, getAuthStateCache()); + break; + case CODING: + authRequest = new AuthCodingRequest(authConfig, getAuthStateCache()); + break; + case CSDN: + authRequest = new AuthCsdnRequest(authConfig, getAuthStateCache()); + break; + case TAOBAO: + authRequest = new AuthTaobaoRequest(authConfig, getAuthStateCache()); + break; + case GOOGLE: + authRequest = new AuthGoogleRequest(authConfig, getAuthStateCache()); + break; + case FACEBOOK: + authRequest = new AuthFacebookRequest(authConfig, getAuthStateCache()); + break; + case DOUYIN: + authRequest = new AuthDouyinRequest(authConfig, getAuthStateCache()); + break; + case LINKEDIN: + authRequest = new AuthLinkedinRequest(authConfig, getAuthStateCache()); + break; + case MICROSOFT: + authRequest = new AuthMicrosoftRequest(authConfig, getAuthStateCache()); + break; + case MI: + authRequest = new AuthMiRequest(authConfig, getAuthStateCache()); + break; + case TOUTIAO: + authRequest = new AuthToutiaoRequest(authConfig, getAuthStateCache()); + break; + case TEAMBITION: + authRequest = new AuthTeambitionRequest(authConfig, getAuthStateCache()); + break; + case PINTEREST: + authRequest = new AuthPinterestRequest(authConfig, getAuthStateCache()); + break; + case RENREN: + authRequest = new AuthRenrenRequest(authConfig, getAuthStateCache()); + break; + case STACK_OVERFLOW: + authRequest = new AuthStackOverflowRequest(authConfig, getAuthStateCache()); + break; + case HUAWEI: + authRequest = new AuthHuaweiRequest(authConfig, getAuthStateCache()); + break; + case KUJIALE: + authRequest = new AuthKujialeRequest(authConfig, getAuthStateCache()); + break; + case GITLAB: + authRequest = new AuthGitlabRequest(authConfig, getAuthStateCache()); + break; + case MEITUAN: + authRequest = new AuthMeituanRequest(authConfig, getAuthStateCache()); + break; + case ELEME: + authRequest = new AuthElemeRequest(authConfig, getAuthStateCache()); + break; + case TWITTER: + authRequest = new AuthTwitterRequest(authConfig, getAuthStateCache()); + break; + default: + break; + } + if (null == authRequest) { + throw new AuthException("未获取到有效的Auth配置"); + } + return authRequest; + } + +} diff --git a/blade-starter-social/src/main/resources/blade-social.yml b/blade-starter-social/src/main/resources/blade-social.yml new file mode 100644 index 0000000..694b1b6 --- /dev/null +++ b/blade-starter-social/src/main/resources/blade-social.yml @@ -0,0 +1,3 @@ +blade: + social: + enabled: false diff --git a/blade-starter-swagger/pom.xml b/blade-starter-swagger/pom.xml new file mode 100644 index 0000000..aeb7648 --- /dev/null +++ b/blade-starter-swagger/pom.xml @@ -0,0 +1,40 @@ + + + 4.0.0 + + BladeX-Tool + org.springblade + ${revision} + + + blade-starter-swagger + ${project.artifactId} + ${project.parent.version} + jar + + + + + org.springblade + blade-core-tool + + + org.springblade + blade-starter-auth + + + + com.github.xiaoymin + knife4j-openapi3-jakarta-spring-boot-starter + + + + org.springblade + blade-core-auto + provided + + + + diff --git a/blade-starter-swagger/src/main/java/org/springblade/core/swagger/EnableSwagger.java b/blade-starter-swagger/src/main/java/org/springblade/core/swagger/EnableSwagger.java new file mode 100644 index 0000000..aee6cca --- /dev/null +++ b/blade-starter-swagger/src/main/java/org/springblade/core/swagger/EnableSwagger.java @@ -0,0 +1,40 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.swagger; + + +import java.lang.annotation.*; + +/** + * Swagger配置开关 + * + * @author Chill + */ +@Documented +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface EnableSwagger { +} diff --git a/blade-starter-swagger/src/main/java/org/springblade/core/swagger/SwaggerAutoConfiguration.java b/blade-starter-swagger/src/main/java/org/springblade/core/swagger/SwaggerAutoConfiguration.java new file mode 100644 index 0000000..75d069e --- /dev/null +++ b/blade-starter-swagger/src/main/java/org/springblade/core/swagger/SwaggerAutoConfiguration.java @@ -0,0 +1,184 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.swagger; + + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.launch.constant.TokenConstant; +import org.springblade.core.tool.utils.CollectionUtil; +import org.springblade.core.tool.utils.NumberUtil; +import org.springdoc.core.configuration.SpringDocConfiguration; +import org.springdoc.core.customizers.GlobalOpenApiCustomizer; +import org.springdoc.core.models.GroupedOpenApi; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * swagger配置 + * + * @author Chill + */ +@Slf4j +@EnableSwagger +@Configuration +@AllArgsConstructor +@AutoConfigureBefore(SpringDocConfiguration.class) +@EnableConfigurationProperties(SwaggerProperties.class) +@ConditionalOnProperty(value = "swagger.enabled", havingValue = "true", matchIfMissing = true) +public class SwaggerAutoConfiguration { + + private static final String DEFAULT_BASE_PATH = "/**"; + private static final List DEFAULT_EXCLUDE_PATH = Arrays.asList("/error", "/actuator/**"); + + private static final String AUTHORIZATION_HEADER = "Authorization"; + private static final String TOKEN_HEADER = TokenConstant.HEADER; + private static final String TENANT_HEADER = "Tenant-Id"; + + /** + * 引入Swagger配置类 + */ + private final SwaggerProperties swaggerProperties; + + /** + * 初始化OpenAPI对象 + */ + @Bean + public OpenAPI openApi() { + // 初始化OpenAPI对象,并设置API的基本信息、安全策略、联系人信息、许可信息以及外部文档链接 + return new OpenAPI() + .components(new Components() + // 添加安全策略,配置API密钥(Token)和鉴权机制 + .addSecuritySchemes(TOKEN_HEADER, + new SecurityScheme() + .type(SecurityScheme.Type.APIKEY) + .in(SecurityScheme.In.HEADER) + .scheme("bearer") + .bearerFormat("JWT") + .name(TOKEN_HEADER) + ) + // 添加安全策略,配置API密钥(Authorization)和鉴权机制 + .addSecuritySchemes(AUTHORIZATION_HEADER, + new SecurityScheme() + .type(SecurityScheme.Type.APIKEY) + .in(SecurityScheme.In.HEADER) + .name(AUTHORIZATION_HEADER) + ) + // 添加安全策略,配置租户ID(Tenant-Id)和鉴权机制 + .addSecuritySchemes(TENANT_HEADER, + new SecurityScheme() + .type(SecurityScheme.Type.APIKEY) + .in(SecurityScheme.In.HEADER) + .name(TENANT_HEADER) + ) + ) + // 设置API文档的基本信息,包括标题、描述、联系方式和许可信息 + .info(new Info() + .title(swaggerProperties.getTitle()) + .description(swaggerProperties.getDescription()) + .termsOfService(swaggerProperties.getTermsOfServiceUrl()) + .contact(new Contact() + .name(swaggerProperties.getContact().getName()) + .email(swaggerProperties.getContact().getEmail()) + .url(swaggerProperties.getContact().getUrl()) + ) + .license(new License() + .name(swaggerProperties.getLicense()) + .url(swaggerProperties.getLicenseUrl()) + ) + .version(swaggerProperties.getVersion()) + ); + } + + /** + * 初始化GlobalOpenApiCustomizer对象 + */ + @Bean + @ConditionalOnMissingBean + public GlobalOpenApiCustomizer orderGlobalOpenApiCustomizer() { + return openApi -> { + if (openApi.getPaths() != null) { + openApi.getPaths().forEach((s, pathItem) -> pathItem.readOperations().forEach(operation -> + operation.addSecurityItem(new SecurityRequirement() + .addList(AUTHORIZATION_HEADER) + .addList(TOKEN_HEADER) + .addList(TENANT_HEADER)))); + } + if (openApi.getTags() != null) { + openApi.getTags().forEach(tag -> { + Map map = new HashMap<>(); + map.put("x-order", NumberUtil.parseFirstInt(tag.getDescription())); + tag.setExtensions(map); + }); + } + }; + } + + /** + * 初始化GroupedOpenApi对象 + */ + @Bean + @ConditionalOnMissingBean + public GroupedOpenApi defaultApi() { + // 如果Swagger配置中的基本路径和排除路径为空,则设置默认的基本路径和排除路径 + if (CollectionUtil.isEmpty(swaggerProperties.getBasePath())) { + swaggerProperties.getBasePath().add(DEFAULT_BASE_PATH); + } + if (CollectionUtil.isEmpty(swaggerProperties.getExcludePath())) { + swaggerProperties.getExcludePath().addAll(DEFAULT_EXCLUDE_PATH); + } + // 获取Swagger配置中的基本路径、排除路径、基本包路径和排除包路径 + List basePath = swaggerProperties.getBasePath(); + List excludePath = swaggerProperties.getExcludePath(); + List basePackages = swaggerProperties.getBasePackages(); + List excludePackages = swaggerProperties.getExcludePackages(); + // 创建并返回GroupedOpenApi对象 + return GroupedOpenApi.builder() + .group("default") + .pathsToMatch(basePath.toArray(new String[0])) + .pathsToExclude(excludePath.toArray(new String[0])) + .packagesToScan(basePackages.toArray(new String[0])) + .packagesToExclude(excludePackages.toArray(new String[0])) + .build(); + } + +} diff --git a/blade-starter-swagger/src/main/java/org/springblade/core/swagger/SwaggerLauncherServiceImpl.java b/blade-starter-swagger/src/main/java/org/springblade/core/swagger/SwaggerLauncherServiceImpl.java new file mode 100644 index 0000000..bf11ec8 --- /dev/null +++ b/blade-starter-swagger/src/main/java/org/springblade/core/swagger/SwaggerLauncherServiceImpl.java @@ -0,0 +1,57 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.swagger; + +import org.springblade.core.auto.service.AutoService; +import org.springblade.core.launch.constant.AppConstant; +import org.springblade.core.launch.service.LauncherService; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.core.Ordered; + +import java.util.Properties; + +/** + * 初始化Swagger配置 + * + * @author Chill + */ +@AutoService(LauncherService.class) +public class SwaggerLauncherServiceImpl implements LauncherService { + @Override + public void launcher(SpringApplicationBuilder builder, String appName, String profile, boolean isLocalDev) { + Properties props = System.getProperties(); + if (profile.equals(AppConstant.PROD_CODE)) { + props.setProperty("knife4j.production", "true"); + } + props.setProperty("knife4j.enable", "true"); + props.setProperty("spring.mvc.pathmatch.matching-strategy", "ANT_PATH_MATCHER"); + } + + @Override + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE; + } +} diff --git a/blade-starter-swagger/src/main/java/org/springblade/core/swagger/SwaggerProperties.java b/blade-starter-swagger/src/main/java/org/springblade/core/swagger/SwaggerProperties.java new file mode 100644 index 0000000..f1b7682 --- /dev/null +++ b/blade-starter-swagger/src/main/java/org/springblade/core/swagger/SwaggerProperties.java @@ -0,0 +1,142 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.swagger; + +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springblade.core.launch.constant.AppConstant; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * SwaggerProperties + * + * @author Chill + */ +@Data +@ConfigurationProperties("swagger") +public class SwaggerProperties { + /** + * 是否开启swagger + */ + private boolean enabled = true; + /** + * swagger会解析的包路径 + **/ + private List basePackages = new ArrayList<>(Collections.singletonList(AppConstant.BASE_PACKAGES)); + /** + * swagger会排除解析的包路径 + **/ + private List excludePackages = new ArrayList<>(); + /** + * swagger会解析的url规则 + **/ + private List basePath = new ArrayList<>(); + /** + * 在basePath基础上需要排除的url规则 + **/ + private List excludePath = new ArrayList<>(); + /** + * 标题 + **/ + private String title = "BladeX 接口文档系统"; + /** + * 描述 + **/ + private String description = "BladeX 接口文档系统"; + /** + * 版本 + **/ + private String version = AppConstant.APPLICATION_VERSION; + /** + * 许可证 + **/ + private String license = "Powered By BladeX"; + /** + * 许可证URL + **/ + private String licenseUrl = "https://license.bladex.cn"; + /** + * 服务条款URL + **/ + private String termsOfServiceUrl = "https://bladex.cn"; + /** + * host信息 + **/ + private String host = ""; + /** + * 联系人信息 + */ + private Contact contact = new Contact(); + /** + * 全局统一鉴权配置 + **/ + private Authorization authorization = new Authorization(); + + @Data + @NoArgsConstructor + public static class Contact { + + /** + * 联系人 + **/ + private String name = "翼宿"; + /** + * 联系人email + **/ + private String email = "bladejava@qq.com"; + /** + * 联系人url + **/ + private String url = "https://gitee.com/smallc"; + + } + + @Data + @NoArgsConstructor + public static class Authorization { + + /** + * 鉴权策略ID,需要和SecurityReferences ID保持一致 + */ + private String name = ""; + + /** + * 需要开启鉴权URL的正则 + */ + private String authRegex = "^.*$"; + + /** + * 接口匹配地址 + */ + private List tokenUrlList = new ArrayList<>(); + } + + +} diff --git a/blade-starter-swagger/src/main/java/org/springblade/core/swagger/SwaggerWebConfiguration.java b/blade-starter-swagger/src/main/java/org/springblade/core/swagger/SwaggerWebConfiguration.java new file mode 100644 index 0000000..73737aa --- /dev/null +++ b/blade-starter-swagger/src/main/java/org/springblade/core/swagger/SwaggerWebConfiguration.java @@ -0,0 +1,51 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.swagger; + + +import org.springblade.core.launch.props.BladePropertySource; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +/** + * swagger资源配置 + * + * @author Chill + */ +@AutoConfiguration +@EnableConfigurationProperties(SwaggerProperties.class) +@BladePropertySource(value = "classpath:/blade-swagger.yml") +public class SwaggerWebConfiguration implements WebMvcConfigurer { + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + registry.addResourceHandler("doc.html").addResourceLocations("classpath:/META-INF/resources/"); + registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/"); + } + +} diff --git a/blade-starter-swagger/src/main/resources/blade-swagger.yml b/blade-starter-swagger/src/main/resources/blade-swagger.yml new file mode 100644 index 0000000..753d885 --- /dev/null +++ b/blade-starter-swagger/src/main/resources/blade-swagger.yml @@ -0,0 +1,45 @@ +#springdoc-openapi项目配置 +springdoc: + swagger-ui: + path: /swagger-ui.html + tags-sorter: order + operations-sorter: order + persist-authorization: true + api-docs: + enabled: true + path: /v3/api-docs + +#knife4j配置 +knife4j: + #启用 + enable: true + #基础认证 + basic: + enable: false + username: blade + password: blade + #增强配置 + setting: + enable-swagger-models: true + enable-document-manage: true + enable-host: false + enable-host-text: http://localhost + enable-request-cache: true + enable-filter-multipart-apis: false + enable-filter-multipart-api-method-type: POST + enable-footer: false + enable-footer-custom: true + language: zh_cn + footer-custom-content: Copyright © 2024 BladeX All Rights Reserved + +#swagger公共信息 +swagger: + title: BladeX 接口文档系统 + description: BladeX 接口文档系统 + license: Powered By BladeX + license-url: https://license.bladex.cn + terms-of-service-url: https://bladex.cn + contact: + name: 翼宿 + email: bladejava@qq.com + url: https://gitee.com/smallc diff --git a/blade-starter-tenant-dynamic/pom.xml b/blade-starter-tenant-dynamic/pom.xml new file mode 100644 index 0000000..71668f4 --- /dev/null +++ b/blade-starter-tenant-dynamic/pom.xml @@ -0,0 +1,48 @@ + + + 4.0.0 + + org.springblade + BladeX-Tool + ${revision} + + + blade-starter-tenant-dynamic + ${project.artifactId} + ${project.parent.version} + jar + + + + + org.springblade + blade-starter-tenant + + + + com.alibaba + druid-spring-boot-3-starter + provided + + + + com.baomidou + dynamic-datasource-spring-boot3-starter + + + + org.apache.shardingsphere + shardingsphere-jdbc-core-spring-boot-starter + provided + + + + org.springblade + blade-core-auto + provided + + + + diff --git a/blade-starter-tenant-dynamic/src/main/java/org/springblade/core/tenant/config/TenantDataSourceConfiguration.java b/blade-starter-tenant-dynamic/src/main/java/org/springblade/core/tenant/config/TenantDataSourceConfiguration.java new file mode 100644 index 0000000..dadba0b --- /dev/null +++ b/blade-starter-tenant-dynamic/src/main/java/org/springblade/core/tenant/config/TenantDataSourceConfiguration.java @@ -0,0 +1,213 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.tenant.config; + +import com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceAutoConfigure; +import com.baomidou.dynamic.datasource.DynamicRoutingDataSource; +import com.baomidou.dynamic.datasource.annotation.DS; +import com.baomidou.dynamic.datasource.aop.DynamicDataSourceAnnotationAdvisor; +import com.baomidou.dynamic.datasource.creator.DataSourceCreator; +import com.baomidou.dynamic.datasource.creator.DataSourceProperty; +import com.baomidou.dynamic.datasource.creator.DefaultDataSourceCreator; +import com.baomidou.dynamic.datasource.creator.druid.DruidDataSourceCreator; +import com.baomidou.dynamic.datasource.event.DataSourceInitEvent; +import com.baomidou.dynamic.datasource.event.EncDataSourceInitEvent; +import com.baomidou.dynamic.datasource.processor.DsJakartaHeaderProcessor; +import com.baomidou.dynamic.datasource.processor.DsJakartaSessionProcessor; +import com.baomidou.dynamic.datasource.processor.DsProcessor; +import com.baomidou.dynamic.datasource.processor.DsSpelExpressionProcessor; +import com.baomidou.dynamic.datasource.provider.DynamicDataSourceProvider; +import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceAutoConfiguration; +import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceCreatorAutoConfiguration; +import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceProperties; +import lombok.AllArgsConstructor; +import lombok.RequiredArgsConstructor; +import org.springblade.core.tenant.dynamic.*; +import org.springframework.beans.factory.SmartInitializingSingleton; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Primary; +import org.springframework.context.annotation.Role; +import org.springframework.core.annotation.Order; +import org.springframework.jdbc.core.JdbcTemplate; + +import javax.sql.DataSource; +import java.util.List; + +import static org.springblade.core.tenant.constant.TenantBaseConstant.TENANT_DYNAMIC_DATASOURCE_PROP; +import static org.springblade.core.tenant.constant.TenantBaseConstant.TENANT_DYNAMIC_GLOBAL_PROP; + +/** + * 多租户数据源配置类 + * + * @author Chill + */ +@RequiredArgsConstructor +@EnableConfigurationProperties({DataSourceProperties.class, DynamicDataSourceProperties.class}) +@AutoConfiguration(before = {DruidDataSourceAutoConfigure.class, DynamicDataSourceAutoConfiguration.class}) +@Import(value = {DynamicDataSourceCreatorAutoConfiguration.class}) +@ConditionalOnProperty(value = TENANT_DYNAMIC_DATASOURCE_PROP, havingValue = "true") +public class TenantDataSourceConfiguration { + + @Bean + @ConditionalOnMissingBean + public DataSourceInitEvent dataSourceInitEvent() { + return new EncDataSourceInitEvent(); + } + + @Bean + @ConditionalOnMissingBean + public DefaultDataSourceCreator dataSourceCreator(List dataSourceCreators, DataSourceInitEvent dataSourceInitEvent, DynamicDataSourceProperties properties) { + DefaultDataSourceCreator creator = new DefaultDataSourceCreator(); + creator.setCreators(dataSourceCreators); + creator.setDataSourceInitEvent(dataSourceInitEvent); + creator.setPublicKey(properties.getPublicKey()); + creator.setLazy(properties.getLazy()); + creator.setP6spy(properties.getP6spy()); + creator.setSeata(properties.getSeata()); + creator.setSeataMode(properties.getSeataMode()); + return creator; + } + + @Bean + @Primary + public DynamicDataSourceProvider dynamicDataSourceProvider(DefaultDataSourceCreator dataSourceCreator, DataSourceProperties dataSourceProperties, DynamicDataSourceProperties dynamicDataSourceProperties) { + String driverClassName = dataSourceProperties.getDriverClassName(); + String url = dataSourceProperties.getUrl(); + String username = dataSourceProperties.getUsername(); + String password = dataSourceProperties.getPassword(); + DataSourceProperty master = dynamicDataSourceProperties.getDatasource().get(dynamicDataSourceProperties.getPrimary()); + if (master != null) { + driverClassName = master.getDriverClassName(); + url = master.getUrl(); + username = master.getUsername(); + password = master.getPassword(); + } + return new TenantDataSourceJdbcProvider(dataSourceCreator, dynamicDataSourceProperties, driverClassName, url, username, password); + } + + @Bean + @Primary + public DataSource dataSource(List providers, DynamicDataSourceProperties dynamicDataSourceProperties) { + DynamicRoutingDataSource dataSource = new DynamicRoutingDataSource(providers); + dataSource.setPrimary(dynamicDataSourceProperties.getPrimary()); + dataSource.setStrict(dynamicDataSourceProperties.getStrict()); + dataSource.setStrategy(dynamicDataSourceProperties.getStrategy()); + dataSource.setP6spy(dynamicDataSourceProperties.getP6spy()); + dataSource.setSeata(dynamicDataSourceProperties.getSeata()); + return dataSource; + } + + @Bean + @ConditionalOnMissingBean + public TenantDataSourceAnnotationInterceptor tenantDataSourceAnnotationInterceptor(DsProcessor dsProcessor, DynamicDataSourceProperties dynamicDataSourceProperties) { + return new TenantDataSourceAnnotationInterceptor(dynamicDataSourceProperties.getAop().getAllowedPublicOnly(), dsProcessor); + } + + @Bean + @ConditionalOnMissingBean + @Role(value = BeanDefinition.ROLE_INFRASTRUCTURE) + public DynamicDataSourceAnnotationAdvisor dynamicDatasourceAnnotationAdvisor(TenantDataSourceAnnotationInterceptor tenantDataSourceAnnotationInterceptor, DynamicDataSourceProperties dynamicDataSourceProperties) { + DynamicDataSourceAnnotationAdvisor advisor = new DynamicDataSourceAnnotationAdvisor(tenantDataSourceAnnotationInterceptor, DS.class); + advisor.setOrder(dynamicDataSourceProperties.getAop().getOrder()); + return advisor; + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(value = TENANT_DYNAMIC_GLOBAL_PROP, havingValue = "true") + public TenantDataSourceGlobalInterceptor tenantDataSourceGlobalInterceptor() { + return new TenantDataSourceGlobalInterceptor(); + } + + @Bean + @ConditionalOnMissingBean + @Role(value = BeanDefinition.ROLE_INFRASTRUCTURE) + @ConditionalOnProperty(value = TENANT_DYNAMIC_GLOBAL_PROP, havingValue = "true") + public TenantDataSourceGlobalAdvisor tenantDataSourceGlobalAdvisor(TenantDataSourceGlobalInterceptor tenantDataSourceGlobalInterceptor, DynamicDataSourceProperties dynamicDataSourceProperties) { + TenantDataSourceGlobalAdvisor advisor = new TenantDataSourceGlobalAdvisor(tenantDataSourceGlobalInterceptor); + advisor.setOrder(dynamicDataSourceProperties.getAop().getOrder() + 1); + return advisor; + } + + @Bean + @ConditionalOnMissingBean + public DsProcessor dsProcessor() { + DsProcessor headerProcessor = new DsJakartaHeaderProcessor(); + DsProcessor sessionProcessor = new DsJakartaSessionProcessor(); + DsTenantIdProcessor tenantIdProcessor = new DsTenantIdProcessor(); + DsSpelExpressionProcessor spelExpressionProcessor = new DsSpelExpressionProcessor(); + headerProcessor.setNextProcessor(sessionProcessor); + sessionProcessor.setNextProcessor(tenantIdProcessor); + tenantIdProcessor.setNextProcessor(spelExpressionProcessor); + return headerProcessor; + } + + @Order + @AutoConfiguration + @AllArgsConstructor + @ConditionalOnProperty(value = TENANT_DYNAMIC_DATASOURCE_PROP, havingValue = "true") + public static class TenantDataSourceAnnotationConfiguration implements SmartInitializingSingleton { + + private final TenantDataSourceAnnotationInterceptor tenantDataSourceAnnotationInterceptor; + + private final DataSource dataSource; + private final DruidDataSourceCreator dataSourceCreator; + private final JdbcTemplate jdbcTemplate; + + @Override + public void afterSingletonsInstantiated() { + TenantDataSourceHolder tenantDataSourceHolder = new TenantDataSourceHolder(dataSource, dataSourceCreator, jdbcTemplate); + tenantDataSourceAnnotationInterceptor.setHolder(tenantDataSourceHolder); + } + } + + @Order + @AutoConfiguration + @AllArgsConstructor + @ConditionalOnProperty(value = TENANT_DYNAMIC_GLOBAL_PROP, havingValue = "true") + public static class TenantDataSourceGlobalConfiguration implements SmartInitializingSingleton { + + private final TenantDataSourceGlobalInterceptor tenantDataSourceGlobalInterceptor; + + private final DataSource dataSource; + private final DruidDataSourceCreator dataSourceCreator; + private final JdbcTemplate jdbcTemplate; + + @Override + public void afterSingletonsInstantiated() { + TenantDataSourceHolder tenantDataSourceHolder = new TenantDataSourceHolder(dataSource, dataSourceCreator, jdbcTemplate); + tenantDataSourceGlobalInterceptor.setHolder(tenantDataSourceHolder); + } + } + +} diff --git a/blade-starter-tenant-dynamic/src/main/java/org/springblade/core/tenant/dynamic/DsTenantIdProcessor.java b/blade-starter-tenant-dynamic/src/main/java/org/springblade/core/tenant/dynamic/DsTenantIdProcessor.java new file mode 100644 index 0000000..2098cad --- /dev/null +++ b/blade-starter-tenant-dynamic/src/main/java/org/springblade/core/tenant/dynamic/DsTenantIdProcessor.java @@ -0,0 +1,51 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.tenant.dynamic; + +import com.baomidou.dynamic.datasource.processor.DsProcessor; +import org.aopalliance.intercept.MethodInvocation; +import org.springblade.core.secure.utils.AuthUtil; + +/** + * 租户动态数据源解析器 + * + * @author Chill + */ +public class DsTenantIdProcessor extends DsProcessor { + + public static final String TENANT_ID_KEY = "#token.tenantId"; + + @Override + public boolean matches(String key) { + return key.equals(TENANT_ID_KEY); + } + + @Override + public String doDetermineDatasource(MethodInvocation invocation, String key) { + return AuthUtil.getTenantId(); + } + +} diff --git a/blade-starter-tenant-dynamic/src/main/java/org/springblade/core/tenant/dynamic/TenantDataSource.java b/blade-starter-tenant-dynamic/src/main/java/org/springblade/core/tenant/dynamic/TenantDataSource.java new file mode 100644 index 0000000..47190e1 --- /dev/null +++ b/blade-starter-tenant-dynamic/src/main/java/org/springblade/core/tenant/dynamic/TenantDataSource.java @@ -0,0 +1,71 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.tenant.dynamic; + +import lombok.Data; + +/** + * 租户数据源 + * + * @author Chill + */ +@Data +public class TenantDataSource { + + /** + * 数据源类型 + */ + private int category; + /** + * 租户ID + */ + private String tenantId; + /** + * 数据源ID + */ + private String datasourceId; + /** + * 驱动类 + */ + private String driverClass; + /** + * 数据库链接 + */ + private String url; + /** + * 数据库账号名 + */ + private String username; + /** + * 数据库密码 + */ + private String password; + /** + * 分库分表配置 + */ + private String shardingConfig; + +} diff --git a/blade-starter-tenant-dynamic/src/main/java/org/springblade/core/tenant/dynamic/TenantDataSourceAnnotationInterceptor.java b/blade-starter-tenant-dynamic/src/main/java/org/springblade/core/tenant/dynamic/TenantDataSourceAnnotationInterceptor.java new file mode 100644 index 0000000..e020d1a --- /dev/null +++ b/blade-starter-tenant-dynamic/src/main/java/org/springblade/core/tenant/dynamic/TenantDataSourceAnnotationInterceptor.java @@ -0,0 +1,59 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.tenant.dynamic; + +import com.baomidou.dynamic.datasource.aop.DynamicDataSourceAnnotationInterceptor; +import com.baomidou.dynamic.datasource.processor.DsProcessor; +import lombok.Setter; +import org.aopalliance.intercept.MethodInvocation; +import org.springblade.core.secure.utils.AuthUtil; +import org.springblade.core.tenant.exception.TenantDataSourceException; + +/** + * 租户数据源切换拦截器 + * + * @author Chill + */ +public class TenantDataSourceAnnotationInterceptor extends DynamicDataSourceAnnotationInterceptor { + + @Setter + private TenantDataSourceHolder holder; + + public TenantDataSourceAnnotationInterceptor(Boolean allowedPublicOnly, DsProcessor dsProcessor) { + super(allowedPublicOnly, dsProcessor); + } + + @Override + public Object invoke(MethodInvocation invocation) throws Throwable { + try { + holder.handleDataSource(AuthUtil.getTenantId()); + return super.invoke(invocation); + } catch (Exception exception) { + throw new TenantDataSourceException(exception.getMessage()); + } + } + +} diff --git a/blade-starter-tenant-dynamic/src/main/java/org/springblade/core/tenant/dynamic/TenantDataSourceGlobalAdvisor.java b/blade-starter-tenant-dynamic/src/main/java/org/springblade/core/tenant/dynamic/TenantDataSourceGlobalAdvisor.java new file mode 100644 index 0000000..db99572 --- /dev/null +++ b/blade-starter-tenant-dynamic/src/main/java/org/springblade/core/tenant/dynamic/TenantDataSourceGlobalAdvisor.java @@ -0,0 +1,84 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.tenant.dynamic; + +import org.aopalliance.aop.Advice; +import org.springframework.aop.Pointcut; +import org.springframework.aop.aspectj.AspectJExpressionPointcut; +import org.springframework.aop.support.AbstractPointcutAdvisor; +import org.springframework.aop.support.ComposablePointcut; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.lang.NonNull; + +import static org.springblade.core.launch.constant.AppConstant.BASE_PACKAGES; + +/** + * 租户数据源全局处理器 + * + * @author Chill + */ +public class TenantDataSourceGlobalAdvisor extends AbstractPointcutAdvisor implements BeanFactoryAware { + + private final Advice advice; + + private final Pointcut pointcut; + + public TenantDataSourceGlobalAdvisor(@NonNull TenantDataSourceGlobalInterceptor tenantDataSourceGlobalInterceptor) { + this.advice = tenantDataSourceGlobalInterceptor; + this.pointcut = buildPointcut(); + } + + @NonNull + @Override + public Pointcut getPointcut() { + return this.pointcut; + } + + @NonNull + @Override + public Advice getAdvice() { + return this.advice; + } + + @Override + public void setBeanFactory(@NonNull BeanFactory beanFactory) throws BeansException { + if (this.advice instanceof BeanFactoryAware) { + ((BeanFactoryAware) this.advice).setBeanFactory(beanFactory); + } + } + + private Pointcut buildPointcut() { + AspectJExpressionPointcut cut = new AspectJExpressionPointcut(); + cut.setExpression( + "(@within(org.springframework.stereotype.Controller) || @within(org.springframework.web.bind.annotation.RestController)) && " + + "(!@annotation(" + BASE_PACKAGES + ".core.tenant.annotation.NonDS) && !@within(" + BASE_PACKAGES + ".core.tenant.annotation.NonDS))" + ); + return new ComposablePointcut((Pointcut) cut); + } + +} diff --git a/blade-starter-tenant-dynamic/src/main/java/org/springblade/core/tenant/dynamic/TenantDataSourceGlobalInterceptor.java b/blade-starter-tenant-dynamic/src/main/java/org/springblade/core/tenant/dynamic/TenantDataSourceGlobalInterceptor.java new file mode 100644 index 0000000..8758dce --- /dev/null +++ b/blade-starter-tenant-dynamic/src/main/java/org/springblade/core/tenant/dynamic/TenantDataSourceGlobalInterceptor.java @@ -0,0 +1,63 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.tenant.dynamic; + +import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder; +import lombok.Setter; +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.springblade.core.secure.utils.AuthUtil; +import org.springblade.core.tenant.exception.TenantDataSourceException; +import org.springblade.core.tool.utils.StringUtil; + +/** + * 租户数据源全局拦截器 + * + * @author Chill + */ +public class TenantDataSourceGlobalInterceptor implements MethodInterceptor { + + @Setter + private TenantDataSourceHolder holder; + + @Override + public Object invoke(MethodInvocation invocation) throws Throwable { + String tenantId = AuthUtil.getTenantId(); + try { + if (StringUtil.isNotBlank(tenantId)) { + holder.handleDataSource(tenantId); + DynamicDataSourceContextHolder.push(tenantId); + } + return invocation.proceed(); + } catch (Exception exception) { + throw new TenantDataSourceException(exception.getMessage()); + } finally { + if (StringUtil.isNotBlank(tenantId)) { + DynamicDataSourceContextHolder.poll(); + } + } + } +} diff --git a/blade-starter-tenant-dynamic/src/main/java/org/springblade/core/tenant/dynamic/TenantDataSourceHolder.java b/blade-starter-tenant-dynamic/src/main/java/org/springblade/core/tenant/dynamic/TenantDataSourceHolder.java new file mode 100644 index 0000000..9d2ba07 --- /dev/null +++ b/blade-starter-tenant-dynamic/src/main/java/org/springblade/core/tenant/dynamic/TenantDataSourceHolder.java @@ -0,0 +1,146 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.tenant.dynamic; + +import com.baomidou.dynamic.datasource.DynamicRoutingDataSource; +import com.baomidou.dynamic.datasource.creator.DataSourceCreator; +import com.baomidou.dynamic.datasource.creator.DataSourceProperty; +import com.baomidou.dynamic.datasource.creator.druid.DruidConfig; +import lombok.AllArgsConstructor; +import org.springblade.core.cache.utils.CacheUtil; +import org.springblade.core.tenant.utils.ShardingUtil; +import org.springblade.core.tool.utils.StringUtil; +import org.springframework.beans.BeanUtils; +import org.springframework.jdbc.core.BeanPropertyRowMapper; +import org.springframework.jdbc.core.JdbcTemplate; + +import javax.sql.DataSource; +import java.util.Set; + +import static org.springblade.core.tenant.constant.TenantBaseConstant.*; + +/** + * 租户数据源核心处理类 + * + * @author Chill + */ +@AllArgsConstructor +public class TenantDataSourceHolder { + + private final DataSource dataSource; + private final DataSourceCreator dataSourceCreator; + private final JdbcTemplate jdbcTemplate; + + /** + * 数据源缓存处理 + * + * @param tenantId 租户ID + */ + public void handleDataSource(String tenantId) { + // 获取储存的数据源集合 + DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource; + Set keys = ds.getDataSources().keySet(); + // 配置不存在则动态添加数据源,以懒加载的模式解决分布式场景的配置同步 + // 为了保证数据完整性,配置后生成数据源缓存,后台便无法修改更换数据源,若一定要修改请迁移数据后重启服务或自行修改底层逻辑 + if (!keys.contains(tenantId)) { + TenantDataSource tenantDataSource = getDataSource(tenantId); + if (tenantDataSource != null) { + int category = tenantDataSource.getCategory(); + if (category == JDBC_CATEGORY) { + // 创建数据源配置 + DataSourceProperty dataSourceProperty = new DataSourceProperty(); + // 拷贝数据源配置 + BeanUtils.copyProperties(tenantDataSource, dataSourceProperty); + // 设置驱动类 + dataSourceProperty.setDriverClassName(tenantDataSource.getDriverClass()); + // 关闭懒加载 + dataSourceProperty.setLazy(Boolean.FALSE); + // 设置SQL校验 + DruidConfig druid = dataSourceProperty.getDruid(); + if (StringUtil.equals(dataSourceProperty.getDriverClassName(), ORACLE_DRIVER_CLASS)) { + druid.setValidationQuery(ORACLE_VALIDATE_STATEMENT); + } else { + druid.setValidationQuery(COMMON_VALIDATE_STATEMENT); + } + // 创建Jdbc数据源 + DataSource dataSource = dataSourceCreator.createDataSource(dataSourceProperty); + // 添加最新数据源 + ds.addDataSource(tenantId, dataSource); + } else if (category == SHARDING_CATEGORY) { + // 创建Sharding数据源 + DataSource dataSource = ShardingUtil.createDataSource(tenantDataSource.getShardingConfig()); + // 添加最新数据源 + ds.addDataSource(tenantId, dataSource); + } + } + } + } + + /** + * 判断租户是否有数据源配置 + * + * @param tenantId 租户ID + */ + private Boolean existDataSource(String tenantId) { + // 将租户是否配置数据源进行缓存,若重新配置会将此缓存清空并在下次请求的时候懒加载 + // 若租户没有配置数据源则会自动使用master数据源,此举是为了避免在没有数据库的时候频繁查询导致缓存击穿 + Boolean exist = CacheUtil.get(TENANT_DATASOURCE_CACHE, TENANT_DATASOURCE_EXIST_KEY, tenantId, Boolean.class, Boolean.FALSE); + if (exist == null) { + TenantDataSource tenantDataSource = jdbcTemplate.queryForObject(TENANT_DATASOURCE_EXIST_STATEMENT, new String[]{tenantId}, new BeanPropertyRowMapper<>(TenantDataSource.class)); + if (tenantDataSource != null && StringUtil.isNotBlank(tenantDataSource.getDatasourceId())) { + exist = Boolean.TRUE; + } else { + exist = Boolean.FALSE; + } + CacheUtil.put(TENANT_DATASOURCE_CACHE, TENANT_DATASOURCE_EXIST_KEY, tenantId, exist, Boolean.FALSE); + } + return exist; + } + + /** + * 获取对应的数据源配置 + * + * @param tenantId 租户ID + */ + private TenantDataSource getDataSource(String tenantId) { + // 不存在租户数据源则返回空,防止缓存击穿 + if (!existDataSource(tenantId)) { + return null; + } + // 获取租户数据源信息 + TenantDataSource tenantDataSource = CacheUtil.get(TENANT_DATASOURCE_CACHE, TENANT_DATASOURCE_KEY, tenantId, TenantDataSource.class, Boolean.FALSE); + if (tenantDataSource == null) { + tenantDataSource = jdbcTemplate.queryForObject(TENANT_DATASOURCE_SINGLE_STATEMENT, new String[]{tenantId}, new BeanPropertyRowMapper<>(TenantDataSource.class)); + if (tenantDataSource != null && StringUtil.isNoneBlank(tenantDataSource.getTenantId(), tenantDataSource.getDriverClass(), tenantDataSource.getUrl(), tenantDataSource.getUsername(), tenantDataSource.getPassword())) { + CacheUtil.put(TENANT_DATASOURCE_CACHE, TENANT_DATASOURCE_KEY, tenantId, tenantDataSource, Boolean.FALSE); + } else { + tenantDataSource = null; + } + } + return tenantDataSource; + } + +} diff --git a/blade-starter-tenant-dynamic/src/main/java/org/springblade/core/tenant/dynamic/TenantDataSourceJdbcProvider.java b/blade-starter-tenant-dynamic/src/main/java/org/springblade/core/tenant/dynamic/TenantDataSourceJdbcProvider.java new file mode 100644 index 0000000..537e244 --- /dev/null +++ b/blade-starter-tenant-dynamic/src/main/java/org/springblade/core/tenant/dynamic/TenantDataSourceJdbcProvider.java @@ -0,0 +1,184 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.tenant.dynamic; + +import com.baomidou.dynamic.datasource.creator.DataSourceProperty; +import com.baomidou.dynamic.datasource.creator.DefaultDataSourceCreator; +import com.baomidou.dynamic.datasource.provider.AbstractJdbcDataSourceProvider; +import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceProperties; +import com.baomidou.dynamic.datasource.toolkit.DsStrUtils; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.tenant.utils.ShardingUtil; +import org.springblade.core.tool.utils.BeanUtil; +import org.springblade.core.tool.utils.StringUtil; + +import javax.sql.DataSource; +import java.sql.*; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import static org.springblade.core.tenant.constant.TenantBaseConstant.*; + +/** + * 租户数据源初始加载 + * + * @author Chill + */ +@Slf4j +public class TenantDataSourceJdbcProvider extends AbstractJdbcDataSourceProvider { + + private final String driverClassName; + private final String url; + private final String username; + private final String password; + private final DefaultDataSourceCreator dataSourceCreator; + private final DynamicDataSourceProperties dynamicDataSourceProperties; + + public TenantDataSourceJdbcProvider(DefaultDataSourceCreator dataSourceCreator, DynamicDataSourceProperties dynamicDataSourceProperties, String driverClassName, String url, String username, String password) { + super(dataSourceCreator, driverClassName, url, username, password); + this.dataSourceCreator = dataSourceCreator; + this.dynamicDataSourceProperties = dynamicDataSourceProperties; + this.driverClassName = driverClassName; + this.url = url; + this.username = username; + this.password = password; + } + + @Override + protected Map executeStmt(Statement statement) throws SQLException { + // 构建数据源集合 + Map map = new HashMap<>(16); + // 构建主数据源 + DataSourceProperty masterProperty = new DataSourceProperty(); + masterProperty.setDriverClassName(driverClassName); + masterProperty.setUrl(url); + masterProperty.setUsername(username); + masterProperty.setPassword(password); + masterProperty.setDruid(dynamicDataSourceProperties.getDruid()); + map.put(dynamicDataSourceProperties.getPrimary(), masterProperty); + // 构建yml数据源 + Map datasource = dynamicDataSourceProperties.getDatasource(); + if (!datasource.isEmpty()) { + map.putAll(datasource); + } + // 构建动态数据源 + ResultSet rs = statement.executeQuery(TENANT_DATASOURCE_GROUP_STATEMENT); + while (rs.next()) { + int category = rs.getInt("category"); + String tenantId = rs.getString("tenantId"); + String driver = rs.getString("driverClass"); + String url = rs.getString("url"); + String username = rs.getString("username"); + String password = rs.getString("password"); + String shardingConfig = rs.getString("shardingConfig"); + // JDBC直连配置 + if (category == JDBC_CATEGORY && StringUtil.isNoneBlank(tenantId, driver, url, username, password)) { + DataSourceProperty jdbcProperty = new DataSourceProperty(); + jdbcProperty.setDriverClassName(driver); + jdbcProperty.setUrl(url); + jdbcProperty.setUsername(username); + jdbcProperty.setPassword(password); + jdbcProperty.setDruid(dynamicDataSourceProperties.getDruid()); + map.put(tenantId, jdbcProperty); + } + // Sharding分库分表配置 + else if (category == SHARDING_CATEGORY && StringUtil.isNotBlank(shardingConfig)) { + DataSource dataSource = ShardingUtil.createDataSource(shardingConfig); + TenantDataSourceProperty shardingProperty = new TenantDataSourceProperty(); + shardingProperty.setTenantId(tenantId); + shardingProperty.setDataSource(dataSource); + map.put(tenantId, shardingProperty); + } + } + return map; + } + + @Override + public Map loadDataSources() { + Connection conn = null; + Statement stmt = null; + try { + // 由于 SPI 的支持,现在已无需显示加载驱动了 + // 但在用户显示配置的情况下,进行主动加载 + if (!DsStrUtils.isEmpty(driverClassName)) { + Class.forName(driverClassName); + log.info("成功加载数据库驱动程序"); + } + conn = DriverManager.getConnection(url, username, password); + log.info("成功获取数据库连接"); + stmt = conn.createStatement(); + Map dataSourcePropertiesMap = executeStmt(stmt); + return createDataSourceMap(dataSourcePropertiesMap); + } catch (Exception e) { + log.error("获取数据库连接失败", e); + } finally { + closeResource(conn); + closeResource(stmt); + } + return null; + } + + /** + * 关闭资源 + * + * @param con 资源 + */ + private static void closeResource(AutoCloseable con) { + if (con != null) { + try { + con.close(); + } catch (SQLException ex) { + log.debug("关闭连接失败", ex); + } catch (Throwable ex) { + log.debug("关闭连接时遇到异常", ex); + } + } + } + + @Override + protected Map createDataSourceMap(Map dataSourcePropertiesMap) { + Map dataSourceMap = new HashMap<>(dataSourcePropertiesMap.size() * 2); + for (Map.Entry item : dataSourcePropertiesMap.entrySet()) { + String dsName = item.getKey(); + DataSourceProperty dataSourceProperty = item.getValue(); + String poolName = dataSourceProperty.getPoolName(); + if (StringUtil.isBlank(poolName)) { + poolName = dsName; + } + TenantDataSourceProperty tenantDataSourceProperty = BeanUtil.copyProperties(dataSourceProperty, TenantDataSourceProperty.class); + DataSource dataSource = Objects.requireNonNull(tenantDataSourceProperty).getDataSource(); + if (dataSource == null) { + dataSourceProperty.setPoolName(poolName); + dataSourceMap.put(dsName, dataSourceCreator.createDataSource(dataSourceProperty)); + } else { + dataSourceMap.put(dsName, dataSource); + } + } + return dataSourceMap; + } + +} diff --git a/blade-starter-tenant-dynamic/src/main/java/org/springblade/core/tenant/dynamic/TenantDataSourceProperty.java b/blade-starter-tenant-dynamic/src/main/java/org/springblade/core/tenant/dynamic/TenantDataSourceProperty.java new file mode 100644 index 0000000..f2d7a68 --- /dev/null +++ b/blade-starter-tenant-dynamic/src/main/java/org/springblade/core/tenant/dynamic/TenantDataSourceProperty.java @@ -0,0 +1,55 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.tenant.dynamic; + +import com.baomidou.dynamic.datasource.creator.DataSourceProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import javax.sql.DataSource; + +/** + * 租户数据源配置类 + * + * @author Chill + */ +@Data +@Accessors(chain = true) +@EqualsAndHashCode(callSuper = true) +public class TenantDataSourceProperty extends DataSourceProperty { + + /** + * 租户id + */ + private String tenantId; + + /** + * 自定义数据源 + */ + private DataSource dataSource; + +} diff --git a/blade-starter-tenant-dynamic/src/main/java/org/springblade/core/tenant/processor/TenantEnvPostProcessor.java b/blade-starter-tenant-dynamic/src/main/java/org/springblade/core/tenant/processor/TenantEnvPostProcessor.java new file mode 100644 index 0000000..a217579 --- /dev/null +++ b/blade-starter-tenant-dynamic/src/main/java/org/springblade/core/tenant/processor/TenantEnvPostProcessor.java @@ -0,0 +1,57 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.tenant.processor; + +import org.springblade.core.auto.annotation.AutoEnvPostProcessor; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.env.EnvironmentPostProcessor; +import org.springframework.core.Ordered; +import org.springframework.core.env.ConfigurableEnvironment; + +/** + * 初始化分库分表配置 + * + * @author Chill + */ +@AutoEnvPostProcessor +public class TenantEnvPostProcessor implements EnvironmentPostProcessor, Ordered { + + private static final String DYNAMIC_DATASOURCE_KEY = "spring.datasource.dynamic.enabled"; + + private static final String AUTOCONFIGURE_EXCLUDE_KEY = "spring.autoconfigure.exclude"; + + + @Override + public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { + environment.getSystemProperties().put(DYNAMIC_DATASOURCE_KEY, "false"); + environment.getSystemProperties().put(AUTOCONFIGURE_EXCLUDE_KEY, "com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceAutoConfigure"); + } + + @Override + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE; + } +} diff --git a/blade-starter-tenant-dynamic/src/main/java/org/springblade/core/tenant/utils/ShardingUtil.java b/blade-starter-tenant-dynamic/src/main/java/org/springblade/core/tenant/utils/ShardingUtil.java new file mode 100644 index 0000000..9cc7e4e --- /dev/null +++ b/blade-starter-tenant-dynamic/src/main/java/org/springblade/core/tenant/utils/ShardingUtil.java @@ -0,0 +1,47 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.tenant.utils; + +import lombok.SneakyThrows; +import org.apache.shardingsphere.driver.api.yaml.YamlShardingSphereDataSourceFactory; + +import javax.sql.DataSource; +import java.nio.charset.StandardCharsets; + +/** + * ShardingUtil + * + * @author Chill + */ +public class ShardingUtil { + + @SneakyThrows + public static DataSource createDataSource(String yamlConfig) { + return YamlShardingSphereDataSourceFactory.createDataSource(yamlConfig.getBytes(StandardCharsets.UTF_8)); + } + + +} diff --git a/blade-starter-tenant/pom.xml b/blade-starter-tenant/pom.xml new file mode 100644 index 0000000..1bfe9eb --- /dev/null +++ b/blade-starter-tenant/pom.xml @@ -0,0 +1,47 @@ + + + 4.0.0 + + BladeX-Tool + org.springblade + ${revision} + + + blade-starter-tenant + ${project.artifactId} + ${project.parent.version} + jar + + + + + org.springblade + blade-starter-mybatis + + + org.springblade + blade-starter-cache + + + + com.alibaba + druid-spring-boot-3-starter + provided + + + + com.baomidou + dynamic-datasource-spring-boot3-starter + provided + + + + org.springblade + blade-core-auto + provided + + + + diff --git a/blade-starter-tenant/src/main/java/org/springblade/core/tenant/BladeTenantHandler.java b/blade-starter-tenant/src/main/java/org/springblade/core/tenant/BladeTenantHandler.java new file mode 100644 index 0000000..dc7559e --- /dev/null +++ b/blade-starter-tenant/src/main/java/org/springblade/core/tenant/BladeTenantHandler.java @@ -0,0 +1,134 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.tenant; + +import com.baomidou.mybatisplus.core.metadata.TableFieldInfo; +import com.baomidou.mybatisplus.core.metadata.TableInfo; +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; +import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.StringValue; +import org.springblade.core.secure.utils.AuthUtil; +import org.springblade.core.tenant.annotation.TableExclude; +import org.springblade.core.tool.constant.BladeConstant; +import org.springblade.core.tool.utils.Func; +import org.springblade.core.tool.utils.SpringUtil; +import org.springblade.core.tool.utils.StringUtil; +import org.springframework.beans.factory.SmartInitializingSingleton; +import org.springframework.context.ApplicationContext; + +import java.util.*; + +/** + * 租户信息处理器 + * + * @author Chill, L.cm + */ +@Slf4j +@RequiredArgsConstructor +public class BladeTenantHandler implements TenantLineHandler, SmartInitializingSingleton { + /** + * 匹配的多租户表 + */ + private final List tenantTableList = new ArrayList<>(); + /** + * 需要排除进行自定义的多租户表 + */ + private final List excludeTableList = Arrays.asList("blade_user", "blade_dept", "blade_role", "blade_tenant", "act_de_model"); + /** + * 多租户配置 + */ + private final BladeTenantProperties tenantProperties; + + /** + * 获取租户ID + * + * @return 租户ID + */ + @Override + public Expression getTenantId() { + return new StringValue(Func.toStr(AuthUtil.getTenantId(), BladeConstant.ADMIN_TENANT_ID)); + } + + /** + * 获取租户字段名称 + * + * @return 租户字段名称 + */ + @Override + public String getTenantIdColumn() { + return tenantProperties.getColumn(); + } + + /** + * 根据表名判断是否忽略拼接多租户条件 + * 默认都要进行解析并拼接多租户条件 + * + * @param tableName 表名 + * @return 是否忽略, true:表示忽略,false:需要解析并拼接多租户条件 + */ + @Override + public boolean ignoreTable(String tableName) { + if (BladeTenantHolder.isIgnore()) { + return true; + } + return !(tenantTableList.contains(tableName) && StringUtil.isNotBlank(AuthUtil.getTenantId())); + } + + @Override + public void afterSingletonsInstantiated() { + ApplicationContext context = SpringUtil.getContext(); + if (tenantProperties.getAnnotationExclude() && context != null) { + Map tables = context.getBeansWithAnnotation(TableExclude.class); + List excludeTables = tenantProperties.getExcludeTables(); + for (Object o : tables.values()) { + TableExclude annotation = o.getClass().getAnnotation(TableExclude.class); + String value = annotation.value(); + excludeTables.add(value); + } + } + List tableInfos = TableInfoHelper.getTableInfos(); + tableFor: + for (TableInfo tableInfo : tableInfos) { + String tableName = tableInfo.getTableName(); + if (tenantProperties.getExcludeTables().contains(tableName) || + excludeTableList.contains(tableName.toLowerCase()) || + excludeTableList.contains(tableName.toUpperCase())) { + continue; + } + List fieldList = tableInfo.getFieldList(); + for (TableFieldInfo fieldInfo : fieldList) { + String column = fieldInfo.getColumn(); + if (tenantProperties.getColumn().equals(column)) { + tenantTableList.add(tableName); + continue tableFor; + } + } + } + } +} diff --git a/blade-starter-tenant/src/main/java/org/springblade/core/tenant/BladeTenantHolder.java b/blade-starter-tenant/src/main/java/org/springblade/core/tenant/BladeTenantHolder.java new file mode 100644 index 0000000..f7197a5 --- /dev/null +++ b/blade-starter-tenant/src/main/java/org/springblade/core/tenant/BladeTenantHolder.java @@ -0,0 +1,58 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.tenant; + +import org.springframework.core.NamedThreadLocal; + +/** + * 租户线程处理 + * + * @author Chill + */ +public class BladeTenantHolder { + + private static final ThreadLocal TENANT_KEY_HOLDER = new NamedThreadLocal("blade-tenant") { + @Override + protected Boolean initialValue() { + return Boolean.FALSE; + } + }; + + public static void setIgnore(Boolean ignore) { + TENANT_KEY_HOLDER.set(ignore); + } + + public static Boolean isIgnore() { + return TENANT_KEY_HOLDER.get(); + } + + + public static void clear() { + TENANT_KEY_HOLDER.remove(); + } + + +} diff --git a/blade-starter-tenant/src/main/java/org/springblade/core/tenant/BladeTenantId.java b/blade-starter-tenant/src/main/java/org/springblade/core/tenant/BladeTenantId.java new file mode 100644 index 0000000..1bb7471 --- /dev/null +++ b/blade-starter-tenant/src/main/java/org/springblade/core/tenant/BladeTenantId.java @@ -0,0 +1,41 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.tenant; + +import org.springblade.core.tool.utils.RandomType; +import org.springblade.core.tool.utils.StringUtil; + +/** + * blade租户id生成器 + * + * @author Chill + */ +public class BladeTenantId implements TenantId { + @Override + public String generate() { + return StringUtil.random(6, RandomType.INT); + } +} diff --git a/blade-starter-tenant/src/main/java/org/springblade/core/tenant/BladeTenantInterceptor.java b/blade-starter-tenant/src/main/java/org/springblade/core/tenant/BladeTenantInterceptor.java new file mode 100644 index 0000000..5e34948 --- /dev/null +++ b/blade-starter-tenant/src/main/java/org/springblade/core/tenant/BladeTenantInterceptor.java @@ -0,0 +1,433 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.tenant; + +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.ExceptionUtils; +import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler; +import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.Parenthesis; +import net.sf.jsqlparser.expression.RowConstructor; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.expression.operators.conditional.AndExpression; +import net.sf.jsqlparser.expression.operators.conditional.OrExpression; +import net.sf.jsqlparser.expression.operators.relational.EqualsTo; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; +import net.sf.jsqlparser.expression.operators.relational.ItemsList; +import net.sf.jsqlparser.expression.operators.relational.MultiExpressionList; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.delete.Delete; +import net.sf.jsqlparser.statement.insert.Insert; +import net.sf.jsqlparser.statement.select.*; +import net.sf.jsqlparser.statement.update.Update; +import net.sf.jsqlparser.statement.update.UpdateSet; +import org.springblade.core.secure.utils.AuthUtil; +import org.springblade.core.tool.utils.CollectionUtil; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * 租户拦截器 + * + * @author Chill + */ +@Data +@ToString(callSuper = true) +@EqualsAndHashCode(callSuper = true) +public class BladeTenantInterceptor extends TenantLineInnerInterceptor { + + /** + * 租户处理器 + */ + private TenantLineHandler tenantLineHandler; + /** + * 租户配置文件 + */ + private BladeTenantProperties tenantProperties; + /** + * 超管需要启用租户过滤的表 + */ + private List adminTenantTables = Arrays.asList("blade_top_menu", "blade_dict_biz"); + + @Override + public void setTenantLineHandler(TenantLineHandler tenantLineHandler) { + super.setTenantLineHandler(tenantLineHandler); + this.tenantLineHandler = tenantLineHandler; + } + + @Override + protected void processInsert(Insert insert, int index, String sql, Object obj) { + // 未启用租户增强,则使用原版逻辑 + if (!tenantProperties.getEnhance()) { + super.processInsert(insert, index, sql, obj); + return; + } + if (tenantLineHandler.ignoreTable(insert.getTable().getName())) { + // 过滤退出执行 + return; + } + List columns = insert.getColumns(); + if (CollectionUtils.isEmpty(columns)) { + // 针对不给列名的insert 不处理 + return; + } + String tenantIdColumn = tenantLineHandler.getTenantIdColumn(); + if (tenantLineHandler.ignoreInsert(columns, tenantIdColumn)) { + // 针对已给出租户列的insert 不处理 + return; + } + columns.add(new Column(tenantIdColumn)); + + // fixed gitee pulls/141 duplicate update + List duplicateUpdateColumns = insert.getDuplicateUpdateExpressionList(); + if (CollectionUtils.isNotEmpty(duplicateUpdateColumns)) { + EqualsTo equalsTo = new EqualsTo(); + equalsTo.setLeftExpression(new StringValue(tenantIdColumn)); + equalsTo.setRightExpression(tenantLineHandler.getTenantId()); + duplicateUpdateColumns.add(equalsTo); + } + + Select select = insert.getSelect(); + if (select != null && (select.getSelectBody() instanceof PlainSelect)) { //fix github issue 4998 修复升级到4.5版本的问题 + this.processInsertSelect(select.getSelectBody(), (String) obj); + } else if (insert.getItemsList() != null) { + // fixed github pull/295 + ItemsList itemsList = insert.getItemsList(); + Expression tenantId = tenantLineHandler.getTenantId(); + if (itemsList instanceof MultiExpressionList) { + ((MultiExpressionList) itemsList).getExpressionLists().forEach(el -> el.getExpressions().add(tenantId)); + } else { + List expressions = ((ExpressionList) itemsList).getExpressions(); + if (CollectionUtils.isNotEmpty(expressions)) {//fix github issue 4998 jsqlparse 4.5 批量insert ItemsList不是MultiExpressionList 了,需要特殊处理 + int len = expressions.size(); + for (int i = 0; i < len; i++) { + Expression expression = expressions.get(i); + if (expression instanceof RowConstructor) { + ((RowConstructor) expression).getExprList().getExpressions().add(tenantId); + } else if (expression instanceof Parenthesis) { + RowConstructor rowConstructor = new RowConstructor() + .withExprList(new ExpressionList(((Parenthesis) expression).getExpression(), tenantId)); + expressions.set(i, rowConstructor); + } else { + if (len - 1 == i) { // (?,?) 只有最后一个expre的时候才拼接tenantId + expressions.add(tenantId); + } + } + } + } else { + expressions.add(tenantId); + } + } + } else { + throw ExceptionUtils.mpe("Failed to process multiple-table update, please exclude the tableName or statementId"); + } + } + + /** + * 处理 PlainSelect + */ + @Override + protected void processPlainSelect(final PlainSelect plainSelect, final String whereSegment) { + //#3087 github + List selectItems = plainSelect.getSelectItems(); + if (CollectionUtils.isNotEmpty(selectItems)) { + selectItems.forEach(selectItem -> processSelectItem(selectItem, whereSegment)); + } + + // 处理 where 中的子查询 + Expression where = plainSelect.getWhere(); + processWhereSubSelect(where, whereSegment); + + // 处理 fromItem + FromItem fromItem = plainSelect.getFromItem(); + List list = processFromItem(fromItem, whereSegment); + List
mainTables = new ArrayList<>(list); + + // 处理 join + List joins = plainSelect.getJoins(); + if (CollectionUtils.isNotEmpty(joins)) { + mainTables = processJoins(mainTables, joins, whereSegment); + } + + // 当有 mainTable 时,进行 where 条件追加 + if (CollectionUtils.isNotEmpty(mainTables) && !doTenantFilters(mainTables)) { + plainSelect.setWhere(builderExpression(where, mainTables, whereSegment)); + } + } + + /** + * update 语句处理 + */ + @Override + protected void processUpdate(Update update, int index, String sql, Object obj) { + final Table table = update.getTable(); + if (tenantLineHandler.ignoreTable(table.getName())) { + // 过滤退出执行 + return; + } + if (doTenantFilter(table.getName())) { + // 过滤退出执行 + return; + } + ArrayList sets = update.getUpdateSets(); + if (!CollectionUtils.isEmpty(sets)) { + sets.forEach(us -> us.getExpressions().forEach(ex -> { + if (ex instanceof SubSelect) { + processSelectBody(((SubSelect) ex).getSelectBody(), (String) obj); + } + })); + } + update.setWhere(this.andExpression(table, update.getWhere(), (String) obj)); + } + + /** + * delete 语句处理 + */ + @Override + protected void processDelete(Delete delete, int index, String sql, Object obj) { + final Table table = delete.getTable(); + if (tenantLineHandler.ignoreTable(table.getName())) { + // 过滤退出执行 + return; + } + if (doTenantFilter(table.getName())) { + // 过滤退出执行 + return; + } + delete.setWhere(this.andExpression(table, delete.getWhere(), (String) obj)); + } + + /** + * delete update 语句 where 处理 + */ + @Override + protected Expression andExpression(Table table, Expression where, final String whereSegment) { + //获得where条件表达式 + final Expression expression = buildTableExpression(table, where, whereSegment); + if (expression == null) { + return where; + } + if (where != null) { + if (where instanceof OrExpression) { + return new AndExpression(new Parenthesis(where), expression); + } else { + return new AndExpression(where, expression); + } + } + return expression; + } + + /** + * 构建租户条件表达式 + * + * @param table 表对象 + * @param where 当前where条件 + * @param whereSegment 所属Mapper对象全路径(在原租户拦截器功能中,这个参数并不需要参与相关判断) + * @return 租户条件表达式 + */ + @Override + public Expression buildTableExpression(final Table table, final Expression where, final String whereSegment) { + //若是忽略的表则不进行数据隔离 + if (tenantLineHandler.ignoreTable(table.getName())) { + return null; + } + //若是超管则不进行数据隔离 + if (doTenantFilter(table.getName())) { + return null; + } + //获得条件表达式 + return new EqualsTo(getAliasColumn(table), tenantLineHandler.getTenantId()); + } + + private List
processFromItem(FromItem fromItem, final String whereSegment) { + // 处理括号括起来的表达式 + while (fromItem instanceof ParenthesisFromItem) { + fromItem = ((ParenthesisFromItem) fromItem).getFromItem(); + } + + List
mainTables = new ArrayList<>(); + // 无 join 时的处理逻辑 + if (fromItem instanceof Table) { + Table fromTable = (Table) fromItem; + mainTables.add(fromTable); + } else if (fromItem instanceof SubJoin) { + // SubJoin 类型则还需要添加上 where 条件 + List
tables = processSubJoin((SubJoin) fromItem, whereSegment); + mainTables.addAll(tables); + } else { + // 处理下 fromItem + processOtherFromItem(fromItem, whereSegment); + } + return mainTables; + } + + /** + * 处理 sub join + * + * @param subJoin subJoin + * @return Table subJoin 中的主表 + */ + private List
processSubJoin(SubJoin subJoin, final String whereSegment) { + List
mainTables = new ArrayList<>(); + if (subJoin.getJoinList() != null) { + List
list = processFromItem(subJoin.getLeft(), whereSegment); + mainTables.addAll(list); + mainTables = processJoins(mainTables, subJoin.getJoinList(), whereSegment); + } + return mainTables; + } + + + /** + * 处理 joins + * + * @param mainTables 可以为 null + * @param joins join 集合 + * @return List
右连接查询的 Table 列表 + */ + private List
processJoins(List
mainTables, List joins, final String whereSegment) { + // join 表达式中最终的主表 + Table mainTable = null; + // 当前 join 的左表 + Table leftTable = null; + + if (mainTables.size() == 1) { + mainTable = mainTables.get(0); + leftTable = mainTable; + } + + //对于 on 表达式写在最后的 join,需要记录下前面多个 on 的表名 + Deque> onTableDeque = new LinkedList<>(); + for (Join join : joins) { + // 处理 on 表达式 + FromItem joinItem = join.getRightItem(); + + // 获取当前 join 的表,subJoint 可以看作是一张表 + List
joinTables = null; + if (joinItem instanceof Table) { + joinTables = new ArrayList<>(); + joinTables.add((Table) joinItem); + } else if (joinItem instanceof SubJoin) { + joinTables = processSubJoin((SubJoin) joinItem, whereSegment); + } + + if (joinTables != null) { + + // 如果是隐式内连接 + if (join.isSimple()) { + mainTables.addAll(joinTables); + continue; + } + + // 当前表是否忽略 + Table joinTable = joinTables.get(0); + + List
onTables = null; + // 如果不要忽略,且是右连接,则记录下当前表 + if (join.isRight()) { + mainTable = joinTable; + mainTables.clear(); + if (leftTable != null) { + onTables = Collections.singletonList(leftTable); + } + } else if (join.isInner()) { + if (mainTable == null) { + onTables = Collections.singletonList(joinTable); + } else { + onTables = Arrays.asList(mainTable, joinTable); + } + mainTable = null; + mainTables.clear(); + } else { + onTables = Collections.singletonList(joinTable); + } + + if (mainTable != null && !mainTables.contains(mainTable)) { + mainTables.add(mainTable); + } + + // 获取 join 尾缀的 on 表达式列表 + Collection originOnExpressions = join.getOnExpressions(); + // 正常 join on 表达式只有一个,立刻处理 + if (originOnExpressions.size() == 1 && onTables != null) { + List onExpressions = new LinkedList<>(); + onExpressions.add(builderExpression(originOnExpressions.iterator().next(), onTables, whereSegment)); + join.setOnExpressions(onExpressions); + leftTable = mainTable == null ? joinTable : mainTable; + continue; + } + // 表名压栈,忽略的表压入 null,以便后续不处理 + onTableDeque.push(onTables); + // 尾缀多个 on 表达式的时候统一处理 + if (originOnExpressions.size() > 1) { + Collection onExpressions = new LinkedList<>(); + for (Expression originOnExpression : originOnExpressions) { + List
currentTableList = onTableDeque.poll(); + if (CollectionUtils.isEmpty(currentTableList)) { + onExpressions.add(originOnExpression); + } else { + onExpressions.add(builderExpression(originOnExpression, currentTableList, whereSegment)); + } + } + join.setOnExpressions(onExpressions); + } + leftTable = joinTable; + } else { + processOtherFromItem(joinItem, whereSegment); + leftTable = null; + } + } + + return mainTables; + } + + + /** + * 判断当前操作是否需要进行过滤 + * + * @param tableName 表名 + */ + public boolean doTenantFilter(String tableName) { + return AuthUtil.isAdministrator() && !adminTenantTables.contains(tableName); + } + + /** + * 判断当前操作是否需要进行过滤 + * + * @param tables 表名 + */ + public boolean doTenantFilters(List
tables) { + List tableNames = tables.stream().map(Table::getName).collect(Collectors.toList()); + return AuthUtil.isAdministrator() && !CollectionUtil.containsAny(adminTenantTables, tableNames); + } + +} diff --git a/blade-starter-tenant/src/main/java/org/springblade/core/tenant/BladeTenantProperties.java b/blade-starter-tenant/src/main/java/org/springblade/core/tenant/BladeTenantProperties.java new file mode 100644 index 0000000..f7af3ff --- /dev/null +++ b/blade-starter-tenant/src/main/java/org/springblade/core/tenant/BladeTenantProperties.java @@ -0,0 +1,80 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.tenant; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.ArrayList; +import java.util.List; + +/** + * 多租户配置 + * + * @author Chill + */ +@Getter +@Setter +@ConfigurationProperties(prefix = "blade.tenant") +public class BladeTenantProperties { + + /** + * 是否增强多租户 + */ + private Boolean enhance = Boolean.FALSE; + + /** + * 是否开启授权码校验 + */ + private Boolean license = Boolean.FALSE; + + /** + * 是否开启动态数据源功能 + */ + private Boolean dynamicDatasource = Boolean.FALSE; + + /** + * 是否开启动态数据源全局扫描 + */ + private Boolean dynamicGlobal = Boolean.FALSE; + + /** + * 多租户字段名称 + */ + private String column = "tenant_id"; + + /** + * 是否开启注解排除 + */ + private Boolean annotationExclude = Boolean.FALSE; + + /** + * 需要排除进行自定义的多租户表 + */ + private List excludeTables = new ArrayList<>(); + +} diff --git a/blade-starter-tenant/src/main/java/org/springblade/core/tenant/TenantId.java b/blade-starter-tenant/src/main/java/org/springblade/core/tenant/TenantId.java new file mode 100644 index 0000000..dcb0606 --- /dev/null +++ b/blade-starter-tenant/src/main/java/org/springblade/core/tenant/TenantId.java @@ -0,0 +1,42 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.tenant; + +/** + * 租户id生成器 + * + * @author Chill + */ +public interface TenantId { + + /** + * 生成自定义租户id + * + * @return tenantId + */ + String generate(); + +} diff --git a/blade-starter-tenant/src/main/java/org/springblade/core/tenant/annotation/NonDS.java b/blade-starter-tenant/src/main/java/org/springblade/core/tenant/annotation/NonDS.java new file mode 100644 index 0000000..9b7a5ed --- /dev/null +++ b/blade-starter-tenant/src/main/java/org/springblade/core/tenant/annotation/NonDS.java @@ -0,0 +1,39 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.tenant.annotation; + +import java.lang.annotation.*; + +/** + * 排除租户数据源自动切换. + * + * @author Chill + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface NonDS { +} diff --git a/blade-starter-tenant/src/main/java/org/springblade/core/tenant/annotation/TableExclude.java b/blade-starter-tenant/src/main/java/org/springblade/core/tenant/annotation/TableExclude.java new file mode 100644 index 0000000..582bb88 --- /dev/null +++ b/blade-starter-tenant/src/main/java/org/springblade/core/tenant/annotation/TableExclude.java @@ -0,0 +1,43 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.tenant.annotation; + +import org.springframework.stereotype.Component; + +import java.lang.annotation.*; + +/** + * 指定租户表排除. + * + * @author Chill + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE}) +@Component +public @interface TableExclude { + String value() default ""; +} diff --git a/blade-starter-tenant/src/main/java/org/springblade/core/tenant/annotation/TenantDS.java b/blade-starter-tenant/src/main/java/org/springblade/core/tenant/annotation/TenantDS.java new file mode 100644 index 0000000..50fa7f3 --- /dev/null +++ b/blade-starter-tenant/src/main/java/org/springblade/core/tenant/annotation/TenantDS.java @@ -0,0 +1,42 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.tenant.annotation; + +import com.baomidou.dynamic.datasource.annotation.DS; + +import java.lang.annotation.*; + +/** + * 指定租户动态数据源切换. + * + * @author Chill + */ +@DS("#token.tenantId") +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface TenantDS { +} diff --git a/blade-starter-tenant/src/main/java/org/springblade/core/tenant/annotation/TenantIgnore.java b/blade-starter-tenant/src/main/java/org/springblade/core/tenant/annotation/TenantIgnore.java new file mode 100644 index 0000000..16efbcb --- /dev/null +++ b/blade-starter-tenant/src/main/java/org/springblade/core/tenant/annotation/TenantIgnore.java @@ -0,0 +1,39 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.tenant.annotation; + +import java.lang.annotation.*; + +/** + * 排除租户逻辑. + * + * @author Chill + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface TenantIgnore { +} diff --git a/blade-starter-tenant/src/main/java/org/springblade/core/tenant/annotation/TenantParamDS.java b/blade-starter-tenant/src/main/java/org/springblade/core/tenant/annotation/TenantParamDS.java new file mode 100644 index 0000000..c5250cc --- /dev/null +++ b/blade-starter-tenant/src/main/java/org/springblade/core/tenant/annotation/TenantParamDS.java @@ -0,0 +1,42 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.tenant.annotation; + +import com.baomidou.dynamic.datasource.annotation.DS; + +import java.lang.annotation.*; + +/** + * 指定租户ID动态数据源切换. + * + * @author Chill + */ +@DS("#tenantId") +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface TenantParamDS { +} diff --git a/blade-starter-tenant/src/main/java/org/springblade/core/tenant/aspect/BladeTenantAspect.java b/blade-starter-tenant/src/main/java/org/springblade/core/tenant/aspect/BladeTenantAspect.java new file mode 100644 index 0000000..7ca8fd3 --- /dev/null +++ b/blade-starter-tenant/src/main/java/org/springblade/core/tenant/aspect/BladeTenantAspect.java @@ -0,0 +1,58 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ + +package org.springblade.core.tenant.aspect; + +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.springblade.core.tenant.BladeTenantHolder; +import org.springblade.core.tenant.annotation.TenantIgnore; + +/** + * 自定义租户切面 + * + * @author Chill + */ +@Slf4j +@Aspect +public class BladeTenantAspect { + + @Around("@annotation(tenantIgnore)") + public Object around(ProceedingJoinPoint point, TenantIgnore tenantIgnore) throws Throwable { + try { + //开启忽略 + BladeTenantHolder.setIgnore(Boolean.TRUE); + //执行方法 + return point.proceed(); + } finally { + //关闭忽略 + BladeTenantHolder.clear(); + } + } + +} diff --git a/blade-starter-tenant/src/main/java/org/springblade/core/tenant/config/TenantConfiguration.java b/blade-starter-tenant/src/main/java/org/springblade/core/tenant/config/TenantConfiguration.java new file mode 100644 index 0000000..ff8eea3 --- /dev/null +++ b/blade-starter-tenant/src/main/java/org/springblade/core/tenant/config/TenantConfiguration.java @@ -0,0 +1,97 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.tenant.config; + +import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler; +import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor; +import lombok.AllArgsConstructor; +import org.springblade.core.mp.config.MybatisPlusConfiguration; +import org.springblade.core.tenant.*; +import org.springblade.core.tenant.aspect.BladeTenantAspect; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; + +/** + * 多租户配置类 + * + * @author Chill + */ +@AllArgsConstructor +@AutoConfiguration(before = MybatisPlusConfiguration.class) +@EnableConfigurationProperties(BladeTenantProperties.class) +public class TenantConfiguration { + + /** + * 自定义多租户处理器 + * + * @param tenantProperties 多租户配置类 + * @return TenantHandler + */ + @Bean + @Primary + public TenantLineHandler bladeTenantHandler(BladeTenantProperties tenantProperties) { + return new BladeTenantHandler(tenantProperties); + } + + /** + * 自定义租户拦截器 + * + * @param tenantHandler 多租户处理器 + * @param tenantProperties 多租户配置类 + * @return BladeTenantInterceptor + */ + @Bean + @Primary + public TenantLineInnerInterceptor tenantLineInnerInterceptor(TenantLineHandler tenantHandler, BladeTenantProperties tenantProperties) { + BladeTenantInterceptor tenantInterceptor = new BladeTenantInterceptor(); + tenantInterceptor.setTenantLineHandler(tenantHandler); + tenantInterceptor.setTenantProperties(tenantProperties); + return tenantInterceptor; + } + + /** + * 自定义租户id生成器 + * + * @return TenantId + */ + @Bean + @ConditionalOnMissingBean(TenantId.class) + public TenantId tenantId() { + return new BladeTenantId(); + } + + /** + * 自定义租户切面 + */ + @Bean + public BladeTenantAspect bladeTenantAspect() { + return new BladeTenantAspect(); + } + +} diff --git a/blade-starter-tenant/src/main/java/org/springblade/core/tenant/constant/TenantBaseConstant.java b/blade-starter-tenant/src/main/java/org/springblade/core/tenant/constant/TenantBaseConstant.java new file mode 100644 index 0000000..a31de0d --- /dev/null +++ b/blade-starter-tenant/src/main/java/org/springblade/core/tenant/constant/TenantBaseConstant.java @@ -0,0 +1,110 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.tenant.constant; + +/** + * 租户常量. + * + * @author Chill + */ +public interface TenantBaseConstant { + + /** + * 租户数据源缓存名 + */ + String TENANT_DATASOURCE_CACHE = "blade:datasource"; + + /** + * 租户数据源缓存键 + */ + String TENANT_DATASOURCE_KEY = "tenant:id:"; + + /** + * 租户数据源缓存键 + */ + String TENANT_DATASOURCE_EXIST_KEY = "tenant:exist:"; + + /** + * 租户动态数据源键 + */ + String TENANT_DYNAMIC_DATASOURCE_PROP = "blade.tenant.dynamic-datasource"; + + /** + * 租户全局动态数据源切面键 + */ + String TENANT_DYNAMIC_GLOBAL_PROP = "blade.tenant.dynamic-global"; + + /** + * 租户是否存在数据源 + */ + String TENANT_DATASOURCE_EXIST_STATEMENT = "select datasource_id from blade_tenant WHERE is_deleted = 0 AND tenant_id = ?"; + + /** + * 租户数据源基础SQL + */ + String TENANT_DATASOURCE_BASE_STATEMENT = "SELECT category, tenant_id as tenantId, driver_class as driverClass, url, username, password, sharding_config as shardingConfig from blade_tenant tenant LEFT JOIN blade_datasource datasource ON tenant.datasource_id = datasource.id "; + + /** + * 租户单数据源SQL + */ + String TENANT_DATASOURCE_SINGLE_STATEMENT = TENANT_DATASOURCE_BASE_STATEMENT + "WHERE tenant.is_deleted = 0 AND tenant.tenant_id = ?"; + + /** + * 租户集动态数据源SQL + */ + String TENANT_DATASOURCE_GROUP_STATEMENT = TENANT_DATASOURCE_BASE_STATEMENT + "WHERE tenant.is_deleted = 0"; + + /** + * 租户未找到返回信息 + */ + String TENANT_DATASOURCE_NOT_FOUND = "未找到租户信息,数据源加载失败!"; + + /** + * oracle驱动类 + */ + String ORACLE_DRIVER_CLASS = "oracle.jdbc.OracleDriver"; + + /** + * oracle校验 + */ + String ORACLE_VALIDATE_STATEMENT = "select 1 from dual"; + + /** + * 通用校验 + */ + String COMMON_VALIDATE_STATEMENT = "select 1"; + + /** + * jdbc数据源分类 + */ + int JDBC_CATEGORY = 1; + + /** + * sharding数据源分类 + */ + int SHARDING_CATEGORY = 2; + +} diff --git a/blade-starter-tenant/src/main/java/org/springblade/core/tenant/exception/TenantDataSourceException.java b/blade-starter-tenant/src/main/java/org/springblade/core/tenant/exception/TenantDataSourceException.java new file mode 100644 index 0000000..a8ca439 --- /dev/null +++ b/blade-starter-tenant/src/main/java/org/springblade/core/tenant/exception/TenantDataSourceException.java @@ -0,0 +1,52 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.tenant.exception; + +/** + * 租户数据源异常 + * + * @author Chill + */ +public class TenantDataSourceException extends RuntimeException { + + public TenantDataSourceException(String message) { + super(message); + } + + /** + * 提高性能 + * + * @return Throwable + */ + @Override + public Throwable fillInStackTrace() { + return this; + } + + public Throwable doFillInStackTrace() { + return super.fillInStackTrace(); + } +} diff --git a/blade-starter-tenant/src/main/java/org/springblade/core/tenant/mp/TenantEntity.java b/blade-starter-tenant/src/main/java/org/springblade/core/tenant/mp/TenantEntity.java new file mode 100644 index 0000000..e1d5fb3 --- /dev/null +++ b/blade-starter-tenant/src/main/java/org/springblade/core/tenant/mp/TenantEntity.java @@ -0,0 +1,49 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.tenant.mp; + + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.mp.base.BaseEntity; + +/** + * 租户基础实体类 + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class TenantEntity extends BaseEntity { + + /** + * 租户ID + */ + @Schema(description = "租户ID") + private String tenantId; + +} diff --git a/blade-starter-trace/pom.xml b/blade-starter-trace/pom.xml new file mode 100644 index 0000000..ea19126 --- /dev/null +++ b/blade-starter-trace/pom.xml @@ -0,0 +1,40 @@ + + + 4.0.0 + + BladeX-Tool + org.springblade + ${revision} + + + blade-starter-trace + ${project.artifactId} + ${project.parent.version} + jar + + + + + org.springblade + blade-core-launch + + + + org.apache.skywalking + apm-toolkit-trace + + + org.apache.skywalking + apm-toolkit-logback-1.x + + + + org.springblade + blade-core-auto + provided + + + + diff --git a/blade-starter-trace/src/main/java/org/springblade/core/trace/TraceAutoConfiguration.java b/blade-starter-trace/src/main/java/org/springblade/core/trace/TraceAutoConfiguration.java new file mode 100644 index 0000000..6d37f14 --- /dev/null +++ b/blade-starter-trace/src/main/java/org/springblade/core/trace/TraceAutoConfiguration.java @@ -0,0 +1,39 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.trace; + +import org.springblade.core.launch.props.BladePropertySource; +import org.springframework.boot.autoconfigure.AutoConfiguration; + +/** + * TraceAutoConfiguration + * + * @author Chill + */ +@AutoConfiguration +@BladePropertySource(value = "classpath:/blade-trace.yml") +public class TraceAutoConfiguration { +} diff --git a/blade-starter-trace/src/main/resources/blade-trace.yml b/blade-starter-trace/src/main/resources/blade-trace.yml new file mode 100644 index 0000000..18350a8 --- /dev/null +++ b/blade-starter-trace/src/main/resources/blade-trace.yml @@ -0,0 +1,5 @@ +#sleuth配置 +spring: + sleuth: + sampler: + percentage: 1.0 diff --git a/blade-starter-transaction/pom.xml b/blade-starter-transaction/pom.xml new file mode 100644 index 0000000..9fdb1f2 --- /dev/null +++ b/blade-starter-transaction/pom.xml @@ -0,0 +1,46 @@ + + + 4.0.0 + + BladeX-Tool + org.springblade + ${revision} + + + blade-starter-transaction + ${project.artifactId} + ${project.parent.version} + jar + + + + + org.springframework.cloud + spring-cloud-commons + + + + org.springblade + blade-starter-mybatis + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-seata + + + io.seata + seata-spring-boot-starter + + + + org.springblade + blade-core-auto + provided + + + + + diff --git a/blade-starter-transaction/src/main/java/org/springblade/core/transaction/annotation/SeataCloudApplication.java b/blade-starter-transaction/src/main/java/org/springblade/core/transaction/annotation/SeataCloudApplication.java new file mode 100644 index 0000000..7940971 --- /dev/null +++ b/blade-starter-transaction/src/main/java/org/springblade/core/transaction/annotation/SeataCloudApplication.java @@ -0,0 +1,49 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.transaction.annotation; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; + +import java.lang.annotation.*; + +/** + * Seata启动注解配置 + * + * @author Chill + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@EnableDiscoveryClient +@SpringBootApplication(exclude = { + DataSourceAutoConfiguration.class +}) +public @interface SeataCloudApplication { + +} diff --git a/blade-starter-transaction/src/main/java/org/springblade/core/transaction/config/TransactionConfiguration.java b/blade-starter-transaction/src/main/java/org/springblade/core/transaction/config/TransactionConfiguration.java new file mode 100644 index 0000000..39a0d8d --- /dev/null +++ b/blade-starter-transaction/src/main/java/org/springblade/core/transaction/config/TransactionConfiguration.java @@ -0,0 +1,43 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.core.transaction.config; + +import org.springblade.core.launch.props.BladePropertySource; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; + +/** + * Seata配置类 + * + * @author Chill + */ +@AutoConfiguration +@Order(Ordered.HIGHEST_PRECEDENCE) +@BladePropertySource(value = "classpath:/blade-transaction.yml") +public class TransactionConfiguration { + +} diff --git a/blade-starter-transaction/src/main/resources/blade-transaction.yml b/blade-starter-transaction/src/main/resources/blade-transaction.yml new file mode 100644 index 0000000..94068b4 --- /dev/null +++ b/blade-starter-transaction/src/main/resources/blade-transaction.yml @@ -0,0 +1,16 @@ +# seata配置 +seata: + tx-service-group: ${blade.name}-group + registry: + type: file + config: + type: file + service: + grouplist: + default: 127.0.0.1:8091 + vgroup-mapping: + blade-tx-group: default + disable-global-transaction: false + client: + rm: + report-success-enable: false diff --git a/blade-starter-transaction/src/main/resources/file.conf b/blade-starter-transaction/src/main/resources/file.conf new file mode 100644 index 0000000..2e9d0b7 --- /dev/null +++ b/blade-starter-transaction/src/main/resources/file.conf @@ -0,0 +1,3 @@ +service { + disableGlobalTransaction = false +} diff --git a/doc/mvn/mvn命令.md b/doc/mvn/mvn命令.md new file mode 100644 index 0000000..804e751 --- /dev/null +++ b/doc/mvn/mvn命令.md @@ -0,0 +1 @@ +mvn install:install-file -Dfile=blade-core-1.0.jar -DgroupId=org.springblade -DartifactId=blade-core -Dversion=1.0 -Dpackaging=jar \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..1d59ee9 --- /dev/null +++ b/pom.xml @@ -0,0 +1,971 @@ + + + 4.0.0 + + org.springblade + BladeX-Tool + ${revision} + pom + + + + 4.0.1.RELEASE + + 17 + 3.11.0 + 1.3.0 + UTF-8 + UTF-8 + + 4.5.0 + 2.16.2 + 4.12.0 + 2.22.0 + 1.5.3 + 2.2 + 2.11.3 + 9.0.0 + 4.3.9 + + 3.5.15 + 3.0.3 + 3.5.5 + 3.5.5 + 4.3.0 + + 5.2.1 + + 8.5.7 + 3.16.1 + 7.12.1 + 5.6.147 + 3.22.12 + 1.12.253 + + 4.5.30 + 1.2.7 + 1.0.6 + + 8.3.0 + 12.2.0.1 + 42.6.2 + 10.2.3.jre17 + 8.1.3.62 + 21.1.0.4.8 + 1.2.22 + + 6.1.5 + 3.2.4 + 3.2.3 + 2023.0.1 + + 2022.0.0.0 + 2.3.1 + + + + blade-bom + blade-core-auto + blade-core-boot + blade-core-cloud + blade-core-context + blade-core-db + blade-core-launch + blade-core-log4j2 + blade-core-oauth2 + blade-core-secure + blade-core-test + blade-core-tool + blade-starter-actuate + blade-starter-api-crypto + blade-starter-auth + blade-starter-cache + blade-starter-datascope + blade-starter-develop + blade-starter-ehcache + blade-starter-excel + blade-starter-flowable + blade-starter-holidays + blade-starter-http + blade-starter-jwt + blade-starter-liteflow + blade-starter-loadbalancer + blade-starter-log + blade-starter-metrics + blade-starter-mongo + blade-starter-mybatis + blade-starter-oss + blade-starter-powerjob + blade-starter-prometheus + blade-starter-redis + blade-starter-report + blade-starter-sharding + blade-starter-sms + blade-starter-social + blade-starter-swagger + blade-starter-tenant + blade-starter-tenant-dynamic + blade-starter-trace + blade-starter-transaction + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring.boot.version} + pom + import + + + org.springframework.cloud + spring-cloud-dependencies + ${spring.cloud.version} + pom + import + + + com.alibaba.cloud + spring-cloud-alibaba-dependencies + ${alibaba.cloud.version} + pom + import + + + org.springframework + spring-framework-bom + ${spring.version} + pom + import + + + com.github.xiaoymin + knife4j-dependencies + ${knife4j.version} + pom + import + + + com.amazonaws + aws-java-sdk-bom + ${aws.sdk.version} + pom + import + + + org.springblade + blade-core-auto + ${revision} + provided + + + org.springblade + blade-core-boot + ${revision} + + + org.springblade + blade-core-cloud + ${revision} + + + org.springblade + blade-core-context + ${revision} + + + org.springblade + blade-core-db + ${revision} + + + org.springblade + blade-core-launch + ${revision} + + + org.springblade + blade-core-log4j2 + ${revision} + + + org.springblade + blade-core-oauth2 + ${revision} + + + org.springblade + blade-core-secure + ${revision} + + + org.springblade + blade-core-test + ${revision} + + + org.springblade + blade-core-tool + ${revision} + + + org.springblade + blade-starter-actuate + ${revision} + + + org.springblade + blade-starter-api-crypto + ${revision} + + + org.springblade + blade-starter-auth + ${revision} + + + org.springblade + blade-starter-cache + ${revision} + + + org.springblade + blade-starter-datascope + ${revision} + + + org.springblade + blade-starter-develop + ${revision} + + + org.springblade + blade-starter-ehcache + ${revision} + + + org.springblade + blade-starter-excel + ${revision} + + + org.springblade + blade-starter-flowable + ${revision} + + + org.springblade + blade-starter-http + ${revision} + + + org.springblade + blade-starter-jwt + ${revision} + + + org.springblade + blade-starter-liteflow + ${revision} + + + org.springblade + blade-starter-log + ${revision} + + + org.springblade + blade-starter-metrics + ${revision} + + + org.springblade + blade-starter-mongo + ${revision} + + + org.springblade + blade-starter-mybatis + ${revision} + + + org.springblade + blade-starter-oss + ${revision} + + + org.springblade + blade-starter-powerjob + ${revision} + + + org.springblade + blade-starter-prometheus + ${revision} + + + org.springblade + blade-starter-redis + ${revision} + + + org.springblade + blade-starter-report + ${revision} + + + org.springblade + blade-starter-sharding + ${revision} + + + org.springblade + blade-starter-loadbalancer + ${revision} + + + org.springblade + blade-starter-sms + ${revision} + + + org.springblade + blade-starter-social + ${revision} + + + org.springblade + blade-starter-swagger + ${revision} + + + org.springblade + blade-starter-tenant + ${revision} + + + org.springblade + blade-starter-tenant-dynamic + ${revision} + + + org.springblade + blade-starter-trace + ${revision} + + + org.springblade + blade-starter-transaction + ${revision} + + + org.springblade + blade-starter-holidays + ${revision} + + + + com.alibaba.nacos + nacos-client + ${alibaba.nacos.version} + + + + org.mybatis + mybatis + ${mybatis.version} + + + org.mybatis + mybatis-spring + ${mybatis.spring.version} + + + com.baomidou + mybatis-plus + ${mybatis.plus.version} + + + com.baomidou + mybatis-plus-spring-boot3-starter + ${mybatis.plus.version} + + + com.baomidou + mybatis-plus-extension + ${mybatis.plus.version} + + + com.baomidou + mybatis-plus-generator + ${mybatis.plus.generator.version} + + + com.baomidou + dynamic-datasource-spring-boot3-starter + ${mybatis.plus.dynamic.version} + + + org.mybatis + mybatis-typehandlers-jsr310 + 1.0.2 + + + + org.apache.shardingsphere + shardingsphere-jdbc-core-spring-boot-starter + ${shardingsphere.version} + + + + io.jsonwebtoken + jjwt-impl + 0.12.5 + + + io.jsonwebtoken + jjwt-jackson + 0.12.5 + + + org.springframework.security + spring-security-jwt + 1.1.1.RELEASE + + + + io.swagger.core.v3 + swagger-annotations + 2.2.19 + + + org.springdoc + springdoc-openapi-starter-webflux-ui + 2.3.0 + + + + de.codecentric + spring-boot-admin-starter-server + ${spring.boot.admin.version} + + + de.codecentric + spring-boot-admin-starter-client + ${spring.boot.admin.version} + + + + com.alibaba + druid + ${druid.version} + + + com.alibaba + druid-spring-boot-3-starter + ${druid.version} + + + + com.mysql + mysql-connector-j + ${mysql.connector.version} + + + + com.oracle + ojdbc7 + ${oracle.connector.version} + + + + org.postgresql + postgresql + ${postgresql.connector.version} + + + + com.microsoft.sqlserver + mssql-jdbc + ${sqlserver.connector.version} + + + + com.dameng + DmJdbcDriver18 + ${dameng.connector.version} + + + + com.yashandb.jdbc + yasdb-jdbc + ${yashandb.connector.version} + + + + io.protostuff + protostuff-core + 1.8.0 + + + io.protostuff + protostuff-runtime + 1.8.0 + + + + org.jsoup + jsoup + 1.12.1 + + + com.squareup.okhttp3 + okhttp + ${okhttp.version} + + + com.squareup.okhttp3 + logging-interceptor + ${okhttp.version} + + + + org.redisson + redisson + 3.17.7 + + + + com.lmax + disruptor + 3.4.2 + + + + net.logstash.logback + logstash-logback-encoder + 6.2 + + + org.codehaus.janino + janino + 3.0.15 + + + + com.xuxueli + xxl-job-core + 2.1.2 + + + + com.github.whvcse + easy-captcha + 1.6.2 + + + + com.fasterxml.jackson.module + jackson-module-jaxb-annotations + ${jackson.version} + + + + com.alibaba + fastjson + 2.0.47 + + + + com.alibaba + easyexcel + 3.3.4 + + + org.apache.poi + ooxml-schemas + 1.4 + compile + + + org.apache.poi + poi + 4.1.2 + compile + + + org.apache.poi + poi-ooxml + 4.1.2 + compile + + + org.apache.poi + poi-ooxml-schemas + 4.1.2 + compile + + + org.apache.poi + poi-scratchpad + 4.1.2 + compile + + + + me.zhyd.oauth + JustAuth + 1.16.6 + + + + com.google.guava + guava + 33.0.0-jre + + + + io.micrometer + micrometer-core + 1.12.4 + + + io.micrometer + micrometer-registry-prometheus + 1.12.4 + + + + com.aliyun.oss + aliyun-sdk-oss + ${aliyun.oss.version} + + + + io.minio + minio + ${minio.version} + + + + com.qiniu + qiniu-java-sdk + ${qiniu.oss.version} + + + + com.qcloud + cos_api + ${tencent.oss.version} + + + + com.huaweicloud + esdk-obs-java + ${esdk.obs.version} + + + + com.aliyun + aliyun-java-sdk-core + ${aliyun.sms.version} + + + + com.yunpian.sdk + yunpian-java-sdk + ${yunpian.sms.version} + + + + com.github.qcloudsms + qcloudsms + ${tencent.sms.version} + + + + com.google.code.findbugs + jsr305 + 3.0.2 + + + org.projectlombok + lombok + 1.18.30 + + + + org.apache.logging.log4j + log4j-api + ${log4j2.version} + + + org.apache.logging.log4j + log4j-core + ${log4j2.version} + + + org.apache.logging.log4j + log4j-jul + ${log4j2.version} + + + org.apache.logging.log4j + log4j-slf4j-impl + ${log4j2.version} + + + org.apache.logging.log4j + log4j-to-slf4j + ${log4j2.version} + + + + ch.qos.logback + logback-classic + ${logback.version} + + + ch.qos.logback + logback-core + ${logback.version} + + + + org.yaml + snakeyaml + ${snakeyaml.version} + + + + com.yomahub + liteflow-spring-boot-starter + ${liteflow.version} + + + + org.apache.skywalking + apm-toolkit-trace + ${skywalking.version} + + + org.apache.skywalking + apm-toolkit-logback-1.x + ${skywalking.version} + + + + tech.powerjob + powerjob-worker-spring-boot-starter + ${powerjob.version} + + + tech.powerjob + powerjob-client + ${powerjob.version} + + + + org.antlr + antlr4-runtime + 4.9.2 + + + + jakarta.servlet + jakarta.servlet-api + 6.0.0 + + + + javax.xml.bind + jaxb-api + 2.3.1 + + + com.sun.xml.bind + jaxb-core + 4.0.5 + + + com.sun.xml.bind + jaxb-impl + 4.0.5 + + + javax.activation + activation + 1.1.1 + + + + + + + org.springframework.boot + spring-boot-starter-aop + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-configuration-processor + true + + + org.springframework.retry + spring-retry + + + org.projectlombok + lombok + provided + + + + + ${project.name} + + + src/main/resources + + + src/main/java + + **/*.xml + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven.plugin.version} + + ${java.version} + ${java.version} + UTF-8 + + -parameters + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.3.0 + + + + true + + true + true + true + + + false + + + + + org.codehaus.mojo + flatten-maven-plugin + ${maven.flatten.version} + + true + oss + + + + flatten + process-resources + + flatten + + + + flatten.clean + clean + + clean + + + + + + + + + + aliyun-repos + Aliyun Public Repository + https://maven.aliyun.com/repository/public + + false + + + + bladex + BladeX Release Repository + https://center.javablade.com/api/packages/blade/maven + + + + + aliyun-plugin + Aliyun Public Plugin + https://maven.aliyun.com/repository/public + + false + + + + + + + bladex + BladeX Release Repository + https://center.javablade.com/api/packages/blade/maven + + + bladex + BladeX Snapshot Repository + https://center.javablade.com/api/packages/blade/maven + + + + + + develop + + + + + org.apache.maven.plugins + maven-source-plugin + 3.0.1 + + true + + + + compile + + jar + + + + + + + + + +