Compare commits

..

16 Commits

Author SHA1 Message Date
Artem Tsebrov 5e0fce2a60 Update testcontainers versions for better docker compatibility (#2)
https://github.com/testcontainers/testcontainers-java/releases/tag/1.18.3
Co-authored-by: Artyom Tsebrov <artem.tsebrov@touchin.ru>
Reviewed-on: #2
2023-08-04 17:38:17 +03:00
Artem Tsebrov a070a94381 Codestyle archunit (#1)
Co-authored-by: Artyom Tsebrov <artem.tsebrov@touchin.ru>
Reviewed-on: #1
2023-04-18 17:17:14 +03:00
Alexander Buntakov 0976db1e47 Merge branch 'feature/kotlin-styleguide-lint' into 'master'
Kotlin Styleguide plugins

See merge request touchinstinct/Backend-common!1
2023-04-06 16:54:03 +00:00
Artem Tsebrov e17454b996 Kotlin Styleguide plugins 2023-04-06 16:54:03 +00:00
TonCherAmi c8ed908656
Mark createdAt as not updatable (#108) 2023-01-13 17:30:42 +03:00
Artyom 20f07a4a9d
Implement "debug" level for logging (#107) 2023-01-10 14:02:39 +03:00
Alexander Buntakov a85e655aba improve simple logger
improve simple logger
2023-01-05 16:48:42 +03:00
Alexander Buntakov 3c837cf1d1
Feature/telegram bot (#106)
* add telegram bot

* remove unused dependencies

* add hasCommand
2022-12-12 14:14:13 +03:00
Ilia Ravin 0fda706e9f
detekt support (#90)
* detekt support

Co-authored-by: Alexander Buntakov <alexander.buntakov@gmail.com>
2022-11-12 18:59:35 +03:00
Artyom 9281a5d213
Push message provider HPK DTO's builder fixes (#105)
* Use correct namings for args, variables and fix builder methods
2022-11-12 17:04:55 +03:00
Ilia Ravin 844c0a9c73
Feature/smart migration (#86)
* smart migrations module

Co-authored-by: Илья Равин <i.ravin@iac.spb.ru>
2022-11-12 16:11:58 +03:00
Artyom d2ddf042e6
Allow to set values only internally (#104)
Prevent users of BaseUuidEntity from "GeneratedValue" misuse which will override even "not-null" values
2022-11-11 15:43:28 +03:00
TonCherAmi 8da83cea86
Implement push message provider mock check method (#103) 2022-11-09 18:23:48 +03:00
Artyom 8e08f8a570
Push message provider for Huawei Push Kit (#102)
* Add basic changes for HPK push provider (#88)
* Add HPK Android-related DTO's (#89)
* Add IOS and Web related DTO's (#93)
* Add common and base DTO's (#94)
* Add HMS Oauth and HMS HPK WebClients and request DTO's (#95)
* Add services for HPS clients and implement push message provider service (#96)
* Cover services with tests (#97)
* Update Readme for new push message provider modules (#98)
* Use new PushMessageSendResult class (#99)
* Various fixes of bean/class naming and tests (#100)
* FIll missing points in readme (#101)
2022-11-08 15:42:25 +03:00
Artyom 7710442654
Fix push message provider fcm module tests and converter (#92)
* Fix test invokations

* Fix DateConverter
2022-11-07 16:23:29 +03:00
TonCherAmi 171cacf30d
Add push message provider mock (#91) 2022-11-03 18:19:14 +03:00
105 changed files with 2581 additions and 268 deletions

219
README.md
View File

@ -19,6 +19,19 @@
implementation("ru.touchin:common")
}
```
## Project Gradle tasks
### Detekt:
- `gradle $project:detekt` - detect not formatted, complex code.
Reports are stored in "$pwd/build/reports/kotlin-detekt-${project.name}.html".
### DiKTat:
- `gradle :diktatCheck` - detect not formatted code of "kt", "kts" files;
- `gradle :diktatFix` - if possible, fix not formatted code. Known issues: full fix may require 1+ launch in order to apply all rules; some rules potentially may break code syntax.
By setting environment variable `TASKS_FILE_REPORT_ENABLED`(true, false) you may configure raw console output or html file as report.
Reports are stored in "$pwd/build/reports/diktat-report.html".
## common
@ -68,6 +81,106 @@
Утилиты для тестирования репозиториев
## codestyle-archunit
Набор правил для поддержки оформления архитектуры.
#### Список доступных правил
- `ru.touchin.codestyle.archunit.rules.ClassNamingArchRules`
- `ru.touchin.codestyle.archunit.rules.ClassPackagingArchRules`
#### Gradle plugin
Настройка и применение совместно с [ArchUnit Gradle Plugin](https://github.com/societe-generale/arch-unit-gradle-plugin).
Действие `checkRules` для проверки соответствие правилам запускается при операциях сборки, по умолчанию.
Вручную можно вызвать командой ``gradle :checkRules`` для нужного модуля.
Добавить его можно следующим образом на примере установки в рутовый gradle.build проекта:
Groovy DSL:
```groovy
buildscript {
dependencies {
classpath "com.societegenerale.commons:arch-unit-gradle-plugin:3.0.0"
}
}
subprojects {
dependencyManagement {
dependencies {
dependency "com.tngtech.archunit:archunit:1.0.1"
}
}
apply plugin: "java"
apply plugin: "com.societegenerale.commons.arch-unit-gradle-plugin"
archUnit {
mainScopePath = "/classes/kotlin/main" // or "/classes/java/main"
testScopePath = "/classes/kotlin/test" // or "/classes/java/test"
var applyType = applyOn("ru.touchin", "main")
configurableRules = [
configurableRule(
"ru.touchin.codestyle.archunit.rules.ClassNamingArchRules",
applyType,
),
configurableRule(
"ru.touchin.codestyle.archunit.rules.ClassPackagingArchRules",
applyType,
),
]
}
dependencies {
archUnitExtraLib "ru.touchin:codestyle-archunit" // or archUnitExtraLib project(":codestyle-archunit")
}
}
```
Kotlin DSL:
```kotlin
plugins {
id("com.societegenerale.commons.arch-unit-gradle-plugin") version "3.0.0"
}
subprojects {
configure<DependencyManagementExtension> {
dependencies {
dependency("com.tngtech.archunit:archunit:1.0.1")
}
}
apply(plugin = "java")
apply(plugin = "com.societegenerale.commons.arch-unit-gradle-plugin")
archUnit {
mainScopePath = "/classes/kotlin/main" // or "/classes/java/main"
testScopePath = "/classes/kotlin/test" // or "/classes/java/test"
configurableRules = listOf(
"ru.touchin.codestyle.archunit.rules.ClassNamingArchRules",
"ru.touchin.codestyle.archunit.rules.ClassPackagingArchRules"
).map { package ->
configurableRule(
package,
applyOn("ru.touchin", "main")
)
}
}
dependencies {
archUnitExtraLib("ru.touchin:codestyle-archunit") // or archUnitExtraLib(project(":codestyle-archunit"))
}
}
```
Отключить проверки на таске помимо конфигурирования `configurableRule` можно также таким образом:
```kotlin
// clear action launch for root project to avoid exception
tasks.checkRules.configure {
actions.clear()
}
```
## logger
Основные компоненты логирования:
@ -216,7 +329,47 @@ server.info:
## push-message-provider
Интерфейсы и компоненты для модулей по обеспечению интеграции с сервисами отправки пуш-уведомлений.
Интерфейсы и компоненты для модулей по обеспечению интеграции с сервисами отправки пуш-уведомлений. Является необходимой зависимостью для использования провайдеров.
Далее рассматривается пример использования подключаемых модулей-провайдеров.
``` kotlin
@Service
class PushSendingService(
private val pushMessageProviderServiceFactory: PushMessageProviderServiceFactory
) {
fun sendPushMessage() {
val yourPushToken = "pushTokenForChecking"
val platform = PlatformType.ANDROID_GOOGLE
val pushMessageProvider: PushMessageProviderService = pushMessageProviderServiceFactory.get(platform)
val result = pushMessageProvider.check( // Проверка валидности токена для обозначения целесообразности отправки
PushTokenCheck(
pushToken = yourPushToken
)
)
if (result.status == PushTokenStatus.VALID) { // Токен валиден, PushMessageProviderService интегрирован в систему
// Отправка пуш-уведомления
pushMessageProvider.send(
PushTokenMessage(
token = yourPushToken,
pushMessageNotification = PushMessageNotification(
title = "Your PushMessage",
description = "Provided by PushMessageProviderService",
imageUrl = null
),
data = mapOf(
"customKey" to "customData"
)
)
)
}
}
}
```
## push-message-provider-fcm
@ -231,7 +384,7 @@ push-message-provider:
IOS:
- FCM
fcm:
appName: # Название приложения
appName: yourAppName
auth:
# Выбранный тип авторизации
client:
@ -258,16 +411,60 @@ C) Данные из файла консоли Firebase, добавляемые
auth:
credentialsData:
type: service_account
projectId: testProjectId
privateKeyId: testPrivateKeyId
projectId: yourProjectId
privateKeyId: yourPrivateKeyId
privateKey: |
-----BEGIN PRIVATE KEY-----
MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBALfBshaLMW2yddmAZJRNXTZzcSbwvY93Dnjj6naWgoBJoB3mOM5bcoyWwBw12A4rwecorz74OUOc6zdqX3j8hwsSyzgAUStKM5PkOvPNRKsI4eXAWU0fmb8h1jyXwftl7EzeBjEMBTpyXkgDk3wLfHN6ciCZrnQndOvS+mMl3b0hAgMBAAECgYEAmIQZByMSrITR0ewCDyFDO52HjhWEkF310hsBkNoNiOMTFZ3vCj/WjJ/W5dM+90wUTYN0KOSnytmkVUNh6K5Yekn+yRg/mBRTwwn88hU6umB8tUqoNz7AyUltAOGyQMWqAAcVgxV+mAp/Y018j69poEHgrW4qKol65/NRZyV7/J0CQQD4rCDjmxGEuA1yMzL2i8NyNl/5vvLVfLcEnVqpHbc1+KfUHZuY7iv38xpzfmErqhCxAXfQ52edq5rXmMIVSbFrAkEAvSvfSSK9XQDJl3NEyfR3BGbsoqKIYOuJAnv4OQPSODZfTNWhc11S8y914qaSWB+Iid9HoLvAIgPH5mrzPzjSowJBAJcw4FZCI+aTmOlEI8ous8gvMy8/X5lZWFUf7s0/2fKgmjmnPsE+ndEFJ6HsxturbLaR8+05pJAClARdRjN3OL0CQGoF+8gmw1ErztCmVyiFbms2MGxagesoN4r/5jg2Tw0YVENg/HMHHCWWNREJ4L2pNsJnNOL+N4oY6mHXEWwesdcCQCUYTfLYxi+Wg/5BSC7fgl/gu0mlx07AzMoMQLDOXdisV5rpxrOoT3BOLBqyccv37AZ3e2gqb8JYyNzO6C0zswQ=
-----END PRIVATE KEY-----
clientEmail: testClientEmail
clientId: testClientId
authUri: testAuthUri
tokenUri: testTokenUri
authProviderX509CertUrl: testAuthProviderX509CertUrl
clientX509CertUrl: testClientX509CertUrl
clientEmail: yourClientEmail
clientId: yourClientId
authUri: yourAuthUri
tokenUri: yourTokenUri
authProviderX509CertUrl: yourAuthProviderX509CertUrl
clientX509CertUrl: yourClientX509CertUrl
```
## push-message-provider-hpk
Модуль по обеспечению интеграции с Huawei Push Kit.
1) Подключение нового провайдера осуществляется при помощи аннотации `@EnablePushMessageProviderHpk`.
2) Для логирования запросов к сервису HPK нужно встроить в контейнер Spring собственный `WebClientLogger` из модуля `logger-spring-web` или же использовать стандартный посредством импорта конфигурации:
``` kotlin
@Import(
SpringLoggerConfiguration::class,
SpringLoggerWebConfiguration::class
)
class YourConfiguration
```
3) Нужно добавить конфигурацию для считывания модулем. Пример файла в формате yaml:
``` yaml
push-message-provider:
platformProviders:
ANDROID_HUAWEI:
- HPK
hpk:
web-services:
client-id: yourClientId
oauth:
client-secret: yourClientSecret
url: https://oauth-login.cloud.huawei.com/oauth2/v3/
http:
connection-timeout: 1s
read-timeout: 10s
write-timeout: 10s
ssl: # Опциональная структура
handshake-timeout: 1s
notify-read-timeout: 1s
notify-flush-timeout: 1s
hpk:
url: https://push-api.cloud.huawei.com/v1/
http:
connection-timeout: 1s
read-timeout: 10s
write-timeout: 10s
ssl: # Опциональная структура
handshake-timeout: 1s
notify-read-timeout: 1s
notify-flush-timeout: 1s
```

View File

@ -1,9 +1,11 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import io.spring.gradle.dependencymanagement.dsl.DependencyManagementExtension
import io.gitlab.arturbosch.detekt.extensions.DetektExtension
import org.jetbrains.kotlin.cli.common.toBooleanLenient
plugins {
kotlin("jvm")
id ("org.springframework.boot") apply false
id("org.springframework.boot") apply false
// IntelliJ
idea
@ -15,6 +17,9 @@ plugins {
// A Gradle plugin that provides Maven-like dependency management and exclusions
// https://docs.spring.io/dependency-management-plugin/docs/current/reference/html/
id("io.spring.dependency-management")
id("io.gitlab.arturbosch.detekt")
id("org.cqfn.diktat.diktat-gradle-plugin")
}
allprojects {
@ -30,6 +35,30 @@ allprojects {
apply(plugin = "idea")
}
diktat {
inputs {
include(
"**/src/**/*.kt",
"*.kts",
"**/*.kts",
"**/src/**/*.kts",
)
exclude(
"**/build/**",
"build/**",
)
}
val tasksFileReportEnabled = project.properties["TASKS_FILE_REPORT_ENABLED"]
?.let { it.toString().toBooleanLenient() }
?: true
reporter = if (tasksFileReportEnabled) "html" else "plain"
output = if (tasksFileReportEnabled) "${project.buildDir}/reports/diktat-report.html" else String()
diktatConfigFile = file("$rootDir/diktat-analysis.yml")
ignoreFailures = true
debug = false
}
subprojects {
println("Enabling Kotlin JVM plugin in project ${project.name}...")
apply(plugin = "org.jetbrains.kotlin.jvm")
@ -40,6 +69,26 @@ subprojects {
println("Enabling Spring Boot Dependency Management in project ${project.name}...")
apply(plugin = "io.spring.dependency-management")
println("Enabling Detekt support in project ${project.name}...")
apply(plugin = "io.gitlab.arturbosch.detekt")
detekt {
config = files("$rootDir/detekt-config.yml")
source = files(
DetektExtension.Companion.DEFAULT_SRC_DIR_JAVA,
DetektExtension.Companion.DEFAULT_SRC_DIR_KOTLIN,
)
reports {
txt.enabled = false
xml.enabled = false
html {
enabled = true
destination = file("${project.buildDir}/reports/kotlin-detekt-${project.name}.html")
}
}
}
configure<DependencyManagementExtension> {
imports {
mavenBom(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)
@ -50,13 +99,15 @@ subprojects {
dependency("ch.qos.logback.contrib:logback-json-classic:0.1.5")
dependency("ch.qos.logback.contrib:logback-jackson:0.1.5")
dependency("org.testcontainers:testcontainers:1.15.1")
dependency("org.testcontainers:postgresql:1.15.1")
dependency("org.testcontainers:junit-jupiter:1.15.1")
dependency("org.testcontainers:testcontainers:1.18.3")
dependency("org.testcontainers:postgresql:1.18.3")
dependency("org.testcontainers:junit-jupiter:1.18.3")
dependency("org.junit.jupiter:junit-jupiter-api:5.4.2")
dependency("org.junit.jupiter:junit-jupiter-params:5.4.2")
dependency("org.junit.jupiter:junit-jupiter-engine:5.4.2")
dependency("com.tngtech.archunit:archunit:1.0.1")
dependency("org.liquibase:liquibase-core:4.4.0")
dependency("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0")
@ -82,7 +133,7 @@ subprojects {
dependencies {
// use for @ConstructorBinding
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation ("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
}

View File

@ -0,0 +1,8 @@
plugins {
id("kotlin")
}
dependencies {
implementation(project(":common"))
implementation("com.tngtech.archunit:archunit")
}

View File

@ -0,0 +1,53 @@
package ru.touchin.codestyle.archunit.rules
import com.tngtech.archunit.lang.ArchRule
import com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes
import com.tngtech.archunit.lang.syntax.ArchRuleDefinition.methods
import ru.touchin.common.exceptions.CommonException
/**
* Set of rules that defines constraint for classes naming.
*/
@Suppress("unused")
object ClassNamingArchRules {
val CLASSES_WHICH_HAVE_SCHEDULED_METHODS_MUST_HAVE_SUFFIX: ArchRule = methods()
.that()
.areAnnotatedWith("org.springframework.scheduling.annotation.Scheduled")
.should().beDeclaredInClassesThat().haveSimpleNameEndingWith("Job")
.allowEmptyShould(true)
.because("Classes that use 'Scheduled' annotation must have name with 'Job' suffix")
val ANNOTATED_SERVICE_CLASSES_MUST_HAVE_SUFFIX: ArchRule = methods()
.that().areAnnotatedWith("org.springframework.stereotype.Service")
.should().beDeclaredInClassesThat().haveSimpleNameContaining("Service")
.allowEmptyShould(true)
.because("Classes that use 'Service' annotation are assignable to class with `Service` suffix")
val ANNOTATED_ENTITY_CLASSES_MUST_HAVE_SUFFIX: ArchRule = classes()
.that().areAnnotatedWith("javax.persistence.Entity")
.should().haveSimpleNameEndingWith("Entity")
.allowEmptyShould(true)
.because("Hibernate 'Entity' classes must have name with 'Entity' suffix")
val EXCEPTION_CLASSES_MUST_HAVE_SUFFIX: ArchRule = classes()
.that().areAssignableTo(Exception::class.java)
.or().areAssignableTo(CommonException::class.java)
.should().haveSimpleNameEndingWith("Exception")
.allowEmptyShould(true)
.because("'Exception' classes must have name with 'Exception' suffix")
val JPAREPOSITORY_CLASSES_MUST_HAVE_SUFFIX: ArchRule = classes()
.that().areAssignableTo("org.springframework.data.jpa.repository.JpaRepository")
.should().haveSimpleNameEndingWith("Repository")
.allowEmptyShould(true)
.because("Classes that use 'JpaRepository' must have name with 'Repository' suffix")
val WEBCLIENT_CLASSES_MUST_HAVE_SUFFIX: ArchRule = classes()
.that().haveFullyQualifiedName("org.springframework.web.reactive.valction.client.WebClient")
.should().onlyBeAccessed().byClassesThat().haveSimpleNameEndingWith("WebClient")
.allowEmptyShould(true)
.because("Classes that use Spring 'WebClient' must have name with 'WebClient' suffix")
}

View File

@ -0,0 +1,56 @@
package ru.touchin.codestyle.archunit.rules
import com.tngtech.archunit.lang.ArchRule
import com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes
/**
* Set of rules that defines constraint for classes placement.
*/
@Suppress("unused")
object ClassPackagingArchRules {
val IMPL_CLASSES_ARE_IN_CORRESPONDING_PACKAGE: ArchRule = classes()
.that().haveSimpleNameEndingWith("Impl")
.should().resideInAPackage("..impl..")
.allowEmptyShould(true)
.because("Implementations of interfaces must reside in package 'impl'")
val ENTITIES_ARE_IN_CORRESPONDING_PACKAGE: ArchRule = classes()
.that().haveSimpleNameEndingWith("Entity")
.should().resideInAPackage("..models..")
.allowEmptyShould(true)
.because("'Entity' must reside in package 'models'")
val REPOSITORIES_ARE_IN_CORRESPONDING_PACKAGE: ArchRule = classes()
.that().haveSimpleNameEndingWith("Repository")
.should().resideInAPackage("..repositories..")
.allowEmptyShould(true)
.because("'Repository' must reside in package 'repositories'")
val CORE_SERVICES_ARE_IN_CORRESPONDING_PACKAGE: ArchRule = classes()
.that().haveSimpleNameEndingWith("CoreService")
.should().resideInAPackage("..services..")
.allowEmptyShould(true)
.because("'CoreService' must reside in package 'services'")
val CONVERTERS_ARE_IN_CORRESPONDING_PACKAGE: ArchRule = classes()
.that().haveSimpleNameEndingWith("Converter")
.should().resideInAPackage("..converters..")
.allowEmptyShould(true)
.because("'Converter' must reside in package 'converters'")
val EXCEPTIONS_ARE_IN_CORRESPONDING_PACKAGE: ArchRule = classes()
.that().haveSimpleNameEndingWith("Exception")
.should().resideInAPackage("..exceptions..")
.allowEmptyShould(true)
.because("'Exception' must reside in package 'exceptions'")
val ENUM_CLASSES_ARE_IN_CORRESPONDING_PACKAGE: ArchRule = classes()
.that().areEnums()
.should().resideInAPackage("..enums..")
.allowEmptyShould(true)
.because("'Enum' must reside in package 'enums'")
}

View File

@ -13,7 +13,7 @@ import javax.persistence.MappedSuperclass
@MappedSuperclass
abstract class BaseEntity : Serializable {
@CreatedDate
@CreatedDate(updatable = false)
lateinit var createdAt: ZonedDateTime
@LastModifiedDate

View File

@ -15,5 +15,9 @@ abstract class BaseUuidIdEntity : BaseEntity() {
@GeneratedValue(generator = "uuid")
@GenericGenerator(name = "uuid", strategy = "uuid2")
open var id: UUID? = null
@Suppress("RedundantSetter")
protected set(id) {
field = id
}
}

View File

@ -0,0 +1,9 @@
package ru.touchin.common.spring.annotations
import org.springframework.context.annotation.Import
import ru.touchin.common.spring.processors.RequiredByBeanDefinitionPostProcessor
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.TYPE, AnnotationTarget.CLASS)
@Import(RequiredByBeanDefinitionPostProcessor::class)
annotation class RequiredBy(vararg val value: String)

View File

@ -0,0 +1,39 @@
package ru.touchin.common.spring.processors
import org.springframework.beans.BeansException
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory
import org.springframework.beans.factory.support.BeanDefinitionRegistry
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor
import org.springframework.stereotype.Component
import ru.touchin.common.spring.annotations.RequiredBy
@Component
class RequiredByBeanDefinitionPostProcessor : BeanDefinitionRegistryPostProcessor {
@Throws(BeansException::class)
override fun postProcessBeanDefinitionRegistry(registry: BeanDefinitionRegistry) {
for (beanName in registry.beanDefinitionNames) {
val beanClassName = registry.getBeanDefinition(beanName).beanClassName?:continue
getDependantBeanNames(beanClassName).forEach { dependantBeanName ->
registry.getBeanDefinition(dependantBeanName).setDependsOn(beanName)
}
}
}
@Throws(BeansException::class)
override fun postProcessBeanFactory(beanFactory: ConfigurableListableBeanFactory) {
// do nothing
}
private fun getDependantBeanNames(beanClassName: String): List<String> {
val beanClass = Class.forName(beanClassName)
var dependantBeans = emptyList<String>()
if (beanClass.isAnnotationPresent(RequiredBy::class.java)) {
dependantBeans = beanClass.getAnnotation(RequiredBy::class.java).value.toList()
}
return dependantBeans
}
}

View File

@ -19,12 +19,12 @@ object StringUtils {
}
nextUpperCase -> {
this.append(char.toUpperCase())
this.append(char.uppercase())
nextUpperCase = false
}
!nextUpperCase -> {
this.append(char.toLowerCase())
else -> {
this.append(char.lowercase())
}
}
}
@ -34,8 +34,8 @@ object StringUtils {
fun String.removeNonPrintableCharacters(): String {
return this
.transliterateCyrillic()
.replace("[\\p{Cntrl}&&[^\r\n\t]]".toRegex(), "")// erases all the ASCII control characters
.replace("\\p{C}".toRegex(), "")// removes non-printable characters from Unicode
.replace("\\p{Cntrl}&&[^\r\n\t]".toRegex(), "") // erases all the ASCII control characters
.replace("\\p{C}".toRegex(), "") // removes non-printable characters from Unicode
.trim()
}

402
detekt-config.yml Normal file
View File

@ -0,0 +1,402 @@
build:
maxIssues: 0
weights:
# complexity: 2
# LongParameterList: 1
# style: 1
# comments: 1
processors:
active: true
exclude:
# - 'FunctionCountProcessor'
# - 'PropertyCountProcessor'
# - 'ClassCountProcessor'
# - 'PackageCountProcessor'
# - 'KtFileCountProcessor'
formatting:
active: true
android: true
autoCorrect: true
MaximumLineLength:
active: true
console-reports:
active: false
exclude:
# - 'ProjectStatisticsReport'
# - 'ComplexityReport'
# - 'NotificationReport'
# - 'FindingsReport'
# - 'BuildFailureReport'
comments:
active: false
CommentOverPrivateFunction:
active: false
CommentOverPrivateProperty:
active: false
EndOfSentenceFormat:
active: false
endOfSentenceFormat: ([.?!][ \t\n\r\f<])|([.?!]$)
UndocumentedPublicClass:
active: false
searchInNestedClass: true
searchInInnerClass: true
searchInInnerObject: true
searchInInnerInterface: true
UndocumentedPublicFunction:
active: false
complexity:
active: true
ComplexCondition:
active: true
threshold: 5
ComplexInterface:
active: false
threshold: 10
includeStaticDeclarations: false
ComplexMethod:
active: true
threshold: 10
ignoreSingleWhenExpression: true
ignoreSimpleWhenEntries: true
LabeledExpression:
active: true
LargeClass:
active: true
threshold: 800
LongMethod:
active: true
threshold: 40
LongParameterList:
active: true
functionThreshold: 10
ignoreDefaultParameters: false
MethodOverloading:
active: false
threshold: 5
NamedArguments:
active: false
threshold: 2
NestedBlockDepth:
active: true
threshold: 4
StringLiteralDuplication:
active: true
threshold: 3
ignoreAnnotation: true
excludeStringsWithLessThan5Characters: true
ignoreStringsRegex: '$^'
TooManyFunctions:
active: true
thresholdInFiles: 20
thresholdInClasses: 20
thresholdInInterfaces: 20
thresholdInObjects: 20
thresholdInEnums: 20
ignoreDeprecated: false
ignorePrivate: false
empty-blocks:
active: true
EmptyCatchBlock:
active: true
allowedExceptionNameRegex: "^(_|(ignore|expected).*)"
EmptyClassBlock:
active: true
EmptyDefaultConstructor:
active: false
EmptyDoWhileBlock:
active: true
EmptyElseBlock:
active: true
EmptyFinallyBlock:
active: true
EmptyForBlock:
active: true
EmptyFunctionBlock:
active: true
ignoreOverridden: false
EmptyIfBlock:
active: true
EmptyInitBlock:
active: true
EmptyKtFile:
active: true
EmptySecondaryConstructor:
active: true
EmptyWhenBlock:
active: true
EmptyWhileBlock:
active: true
exceptions:
active: true
ExceptionRaisedInUnexpectedLocation:
active: true
methodNames: 'toString,hashCode,equals,finalize'
InstanceOfCheckForException:
active: false
NotImplementedDeclaration:
active: true
PrintStackTrace:
active: true
RethrowCaughtException:
active: false
ReturnFromFinally:
active: false
SwallowedException:
active: false
ThrowingExceptionFromFinally:
active: false
ThrowingExceptionInMain:
active: false
ThrowingExceptionsWithoutMessageOrCause:
active: true
exceptions: 'IllegalArgumentException,IllegalStateException,IOException'
ThrowingNewInstanceOfSameException:
active: false
TooGenericExceptionCaught:
active: false
exceptionNames:
- ArrayIndexOutOfBoundsException
- Error
- Exception
- IllegalMonitorStateException
- NullPointerException
- IndexOutOfBoundsException
- RuntimeException
- Throwable
allowedExceptionNameRegex: "^(_|(ignore|expected).*)"
TooGenericExceptionThrown:
active: true
exceptionNames:
- Error
- Exception
- Throwable
- RuntimeException
naming:
active: true
ClassNaming:
active: true
classPattern: '[A-Z$][a-zA-Z0-9$]*'
ConstructorParameterNaming:
active: true
parameterPattern: '[a-z][A-Za-z0-9]*'
privateParameterPattern: '[a-z][A-Za-z0-9]*'
excludeClassPattern: '$^'
EnumNaming:
active: true
enumEntryPattern: '^[A-Z][_a-zA-Z0-9]*'
ForbiddenClassName:
active: false
forbiddenName: ''
FunctionMaxLength:
active: false
maximumFunctionNameLength: 30
FunctionMinLength:
active: false
minimumFunctionNameLength: 3
FunctionNaming:
active: true
functionPattern: '^([a-z$][a-zA-Z$0-9]*)|(`.*`)$'
excludeClassPattern: '$^'
ignoreOverridden: true
FunctionParameterNaming:
active: true
parameterPattern: '[a-z][A-Za-z0-9]*'
excludeClassPattern: '$^'
ignoreOverridden: true
MatchingDeclarationName:
active: true
MemberNameEqualsClassName:
active: false
ignoreOverridden: true
ObjectPropertyNaming:
active: true
constantPattern: '[A-Za-z][_A-Za-z0-9]*'
propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
privatePropertyPattern: '[A-Za-z][_A-Za-z0-9]*'
PackageNaming:
active: false
packagePattern: '^[a-z]+(\.[a-z][a-z0-9]*)*$'
TopLevelPropertyNaming:
active: true
constantPattern: '[A-Z][_A-Z0-9]*'
propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
privatePropertyPattern: '[A-Za-z][_A-Za-z0-9]*'
VariableMaxLength:
active: false
maximumVariableNameLength: 64
VariableMinLength:
active: false
minimumVariableNameLength: 1
VariableNaming:
active: true
variablePattern: '[a-z][A-Za-z0-9]*'
privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*'
excludeClassPattern: '$^'
ignoreOverridden: true
performance:
active: true
ArrayPrimitive:
active: false
ForEachOnRange:
active: true
SpreadOperator:
active: false
UnnecessaryTemporaryInstantiation:
active: true
potential-bugs:
active: true
DuplicateCaseInWhenExpression:
active: true
EqualsAlwaysReturnsTrueOrFalse:
active: false
EqualsWithHashCodeExist:
active: true
ExplicitGarbageCollectionCall:
active: true
InvalidRange:
active: true
IteratorHasNextCallsNextMethod:
active: false
IteratorNotThrowingNoSuchElementException:
active: false
LateinitUsage:
active: false
excludeAnnotatedProperties: ""
ignoreOnClassesPattern: ""
UnconditionalJumpStatementInLoop:
active: false
UnreachableCode:
active: true
UnsafeCallOnNullableType:
active: false
UnsafeCast:
active: false
UselessPostfixExpression:
active: false
WrongEqualsTypeParameter:
active: false
style:
active: true
CollapsibleIfStatements:
active: false
DataClassContainsFunctions:
active: false
conversionFunctionPrefix: 'to'
DestructuringDeclarationWithTooManyEntries:
active: true
maxDestructuringEntries: 5
EqualsNullCall:
active: true
ExplicitItLambdaParameter:
active: true
ExpressionBodySyntax:
active: false
includeLineWrapping: false
ForbiddenComment:
active: true
values: 'STOPSHIP:'
ForbiddenImport:
active: false
imports: ''
ForbiddenVoid:
active: false
FunctionOnlyReturningConstant:
active: false
ignoreOverridableFunction: true
excludedFunctions: 'describeContents'
LoopWithTooManyJumpStatements:
active: false
maxJumpCount: 1
MagicNumber:
active: false
ignoreNumbers: '-1,0,1,2'
ignoreHashCodeFunction: true
ignorePropertyDeclaration: false
ignoreConstantDeclaration: true
ignoreCompanionObjectPropertyDeclaration: true
ignoreAnnotation: false
ignoreNamedArgument: true
ignoreEnums: false
MandatoryBracesIfStatements:
active: true
MaxLineLength:
active: true
maxLineLength: 150
excludePackageStatements: true
excludeImportStatements: true
excludeCommentStatements: false
MayBeConst:
active: true
ModifierOrder:
active: true
NestedClassesVisibility:
active: false
NewLineAtEndOfFile:
active: true
NoTabs:
active: true
OptionalAbstractKeyword:
active: true
OptionalUnit:
active: false
OptionalWhenBraces:
active: false
PreferToOverPairSyntax:
active: true
ProtectedMemberInFinalClass:
active: true
RedundantVisibilityModifierRule:
active: true
ReturnCount:
active: true
max: 3
excludedFunctions: "equals"
SafeCast:
active: true
SerialVersionUIDInSerializableClass:
active: false
SpacingBetweenPackageAndImports:
active: true
ThrowsCount:
active: true
max: 3
TrailingWhitespace:
active: true
UnnecessaryAbstractClass:
active: false
excludeAnnotatedClasses: "dagger.Module"
UnnecessaryApply:
active: true
UnnecessaryInheritance:
active: true
UnnecessaryLet:
active: true
UnnecessaryParentheses:
active: true
UntilInsteadOfRangeTo:
active: true
UnusedImports:
active: true
UnusedPrivateClass:
active: true
UnusedPrivateMember:
active: true
allowedNames: "(_|ignored|expected|serialVersionUID)"
UtilityClassWithPublicConstructor:
active: false
VarCouldBeVal:
active: true
WildcardImport:
active: true
excludeImports: 'java.util.*,kotlinx.android.synthetic.*'

536
diktat-analysis.yml Normal file
View File

@ -0,0 +1,536 @@
#### ENABLED RULES ####
# Checks that there is no more than one statement per line
- name: MORE_THAN_ONE_STATEMENT_PER_LINE
enabled: true
# Checks that line breaks follow code style guide: rule 3.6
- name: WRONG_NEWLINES
enabled: true
configuration:
# If the number of parameters on one line is more than this threshold, all parameters will be placed on separate lines.
maxParametersInOneLine: 2
# 3 by default.
maxCallsInOneLine: 2
# Checks that indentation is correct
- name: WRONG_INDENTATION
enabled: true
configuration:
# Is newline at the end of a file needed
newlineAtEnd: true
# If true: in parameter list when parameters are split by newline they are indented with two indentations instead of one
extendedIndentOfParameters: false
# If true: if first parameter in parameter list is on the same line as opening parenthesis, then other parameters can be aligned with it
alignedParameters: true
# If true, expression bodies which begin on a separate line are indented
# using a continuation indent. The default is false.
#
# This flag is called CONTINUATION_INDENT_FOR_EXPRESSION_BODIES in IDEA and
# ij_kotlin_continuation_indent_for_expression_bodies in .editorconfig.
extendedIndentForExpressionBodies: false
# If true: if expression is split by newline after operator like +/-/`*`, then the next line is indented with two indentations instead of one
extendedIndentAfterOperators: true
# If true: when dot qualified expression starts on a new line, this line will be indented with two indentations instead of one
extendedIndentBeforeDot: false
# The indentation size for each file
indentationSize: 4
# Checks that braces are used in if, else, when, for, do, and while statements. Exception: single line ternary operator statement
- name: NO_BRACES_IN_CONDITIONALS_AND_LOOPS
enabled: true
# Checks that annotation is on a single line
- name: ANNOTATION_NEW_LINE
enabled: true
# Checks that the long lambda has parameters
- name: TOO_MANY_LINES_IN_LAMBDA
enabled: true
configuration:
maxLambdaLength: 1 # max length of lambda without parameters
# Checks that the line length is < lineLength parameter
- name: LONG_LINE
enabled: true
configuration:
lineLength: '120'
# Checks trailing comma
- name: TRAILING_COMMA
enabled: true
configuration:
# VALUE_ARGUMENT
valueArgument: true
# VALUE_PARAMETER
valueParameter: true
# REFERENCE_EXPRESSION
indices: true
# WHEN_CONDITION_WITH_EXPRESSION
whenConditions: true
# STRING_TEMPLATE
collectionLiteral: true
# TYPE_PROJECTION
typeArgument: true
# TYPE_PARAMETER
typeParameter: true
# DESTRUCTURING_DECLARATION_ENTRY
destructuringDeclaration: true
#### DISABLED DEFAULT RULES ####
# Checks that the Class/Enum/Interface name matches Pascal case
- name: CLASS_NAME_INCORRECT
enabled: false
# all code blocks with MyAnnotation will be ignored and not checked
ignoreAnnotated: [ MyAnnotation ]
# Checks that CONSTANT (treated as const val from companion object or class level) is in non UPPER_SNAKE_CASE
- name: CONSTANT_UPPERCASE
enabled: false
# Checks that enum value is in upper SNAKE_CASE or in PascalCase depending on the config. UPPER_SNAKE_CASE is the default, but can be changed by 'enumStyle' config
- name: ENUM_VALUE
enabled: false
configuration:
# Two options: SNAKE_CASE (default), PascalCase
enumStyle: SNAKE_CASE
# Checks that class which extends any Exception class has Exception suffix
- name: EXCEPTION_SUFFIX
enabled: false
# Checks that file name has extension
- name: FILE_NAME_INCORRECT
enabled: false
# Checks that file name matches class name, if it is only one class in file
- name: FILE_NAME_MATCH_CLASS
enabled: false
# Checks that functions/methods which return boolean have special prefix like "is/should/e.t.c"
- name: FUNCTION_BOOLEAN_PREFIX
enabled: false
configuration:
allowedPrefixes: "" # A list of functions that return boolean and are allowed to use. Input is in a form "foo, bar".
# Checks that function/method name is in lowerCamelCase
- name: FUNCTION_NAME_INCORRECT_CASE
enabled: false
# Checks that typealias name is in PascalCase
- name: TYPEALIAS_NAME_INCORRECT_CASE
enabled: false
# Checks that generic name doesn't contain more than 1 letter (capital). It can be followed by numbers, example: T12, T
- name: GENERIC_NAME
enabled: false
# Identifier length should be in range [2,64] except names that used in industry like {i, j} and 'e' for catching exceptions
- name: IDENTIFIER_LENGTH
enabled: false
# Checks that the object matches PascalCase
- name: OBJECT_NAME_INCORRECT
enabled: false
# Checks that package name is in correct (lower) case
- name: PACKAGE_NAME_INCORRECT_CASE
enabled: false
# Checks that package name starts with the company's domain
- name: PACKAGE_NAME_INCORRECT_PREFIX
enabled: false
# Checks that package name does not have incorrect symbols like underscore or non-ASCII letters/digits
- name: PACKAGE_NAME_INCORRECT_SYMBOLS
enabled: false
# Checks that the path for a file matches with a package name
- name: PACKAGE_NAME_INCORRECT_PATH
enabled: false
# Checks that package name is in the file
- name: PACKAGE_NAME_MISSING
enabled: false
# Checks that variable does not have prefix (like mVariable or M_VARIABLE)
- name: VARIABLE_HAS_PREFIX
enabled: false
# Checks that variable does not contain one single letter, only exceptions are fixed names that used in industry like {i, j}
- name: VARIABLE_NAME_INCORRECT
enabled: false
# Checks that the name of variable is in lowerCamelCase and contains only ASCII letters
- name: VARIABLE_NAME_INCORRECT_FORMAT
enabled: false
# Checks that functions have kdoc
- name: MISSING_KDOC_ON_FUNCTION
enabled: false
# Checks that on file level internal or public class or function has missing KDoc
- name: MISSING_KDOC_TOP_LEVEL
enabled: false
# Checks that accessible internal elements (protected, public, internal) in a class are documented
- name: MISSING_KDOC_CLASS_ELEMENTS
enabled: false
# Checks that accessible method parameters are documented in KDoc
- name: KDOC_WITHOUT_PARAM_TAG
enabled: false
# Checks that accessible method explicit return type is documented in KDoc
- name: KDOC_WITHOUT_RETURN_TAG
enabled: false
# Checks that accessible method throw keyword is documented in KDoc
- name: KDOC_WITHOUT_THROWS_TAG
enabled: false
# Checks that KDoc is not empty
- name: KDOC_EMPTY_KDOC
enabled: false
# Checks that underscore is correctly used to split package naming
- name: INCORRECT_PACKAGE_SEPARATOR
enabled: false
# Checks that code block doesn't contain kdoc comments
- name: COMMENTED_BY_KDOC
enabled: false
# Checks that there is no @deprecated tag in kdoc
- name: KDOC_NO_DEPRECATED_TAG
enabled: false
# Checks that there is no empty content in kdoc tags
- name: KDOC_NO_EMPTY_TAGS
enabled: false
# Checks that there is only one space after kdoc tag
- name: KDOC_WRONG_SPACES_AFTER_TAG
enabled: false
# Checks tags order in kDoc. `@param`, `@return`, `@throws`
- name: KDOC_WRONG_TAGS_ORDER
enabled: false
# Checks that there is no newline of empty KDoc line (with leading asterisk) between `@param`, `@return`, `@throws` tags
- name: KDOC_NO_NEWLINES_BETWEEN_BASIC_TAGS
enabled: false
# Checks that block of tags @param, @return, @throws is separated from previous part of KDoc by exactly one empty line
- name: KDOC_NEWLINES_BEFORE_BASIC_TAGS
enabled: false
# Checks that special tags `@apiNote`, `@implNote`, `@implSpec` have exactly one empty line after
- name: KDOC_NO_NEWLINE_AFTER_SPECIAL_TAGS
enabled: false
# Checks that kdoc does not contain @author tag or date
- name: KDOC_CONTAINS_DATE_OR_AUTHOR
enabled: false
configuration:
versionRegex: \d+\.\d+\.\d+[-.\w\d]*
# Checks that KDoc does not contain single line with words 'return', 'get' or 'set'
- name: KDOC_TRIVIAL_KDOC_ON_FUNCTION
enabled: false
# Checks that there is newline after header KDoc
- name: HEADER_WRONG_FORMAT
enabled: false
# Checks that file with zero or >1 classes has header KDoc
- name: HEADER_MISSING_IN_NON_SINGLE_CLASS_FILE
enabled: false
# Checks that copyright exists on top of file and is properly formatted (as a block comment)
- name: HEADER_MISSING_OR_WRONG_COPYRIGHT
enabled: false
configuration:
isCopyrightMandatory: false
copyrightText: 'Copyright (c) Your Company Name Here. 2010-;@currYear;'
# Checks that header kdoc is located before package directive
- name: HEADER_NOT_BEFORE_PACKAGE
enabled: false
# Checks that file does not contain lines > maxSize
- name: FILE_IS_TOO_LONG
enabled: false
configuration:
# number of lines
maxSize: '2000'
# Checks that file does not contain commented out code
- name: COMMENTED_OUT_CODE
enabled: false
# Checks that file does not contain only comments, imports and package directive
- name: FILE_CONTAINS_ONLY_COMMENTS
enabled: false
# Orders imports alphabetically
- name: FILE_UNORDERED_IMPORTS
enabled: false
configuration:
# use logical imports grouping with sorting inside of a group
useRecommendedImportsOrder: true
# Checks that general order of code parts is right
- name: FILE_INCORRECT_BLOCKS_ORDER
enabled: false
# Checks that there is exactly one line between code blocks
- name: FILE_NO_BLANK_LINE_BETWEEN_BLOCKS
enabled: false
# Checks that there is no wildcard imports. Exception: allowedWildcards
- name: FILE_WILDCARD_IMPORTS
enabled: false
configuration:
allowedWildcards: "" # Allowed wildcards for imports (e.g. "import org.cqfn.diktat.*, import org.jetbrains.kotlin.*")
useRecommendedImportsOrder: true
# Checks unused imports
- name: UNUSED_IMPORT
enabled: false
configuration:
deleteUnusedImport: true
# Checks that the declaration part of a class-like code structures (class/interface/etc.) is in the proper order
- name: WRONG_ORDER_IN_CLASS_LIKE_STRUCTURES
enabled: false
# Checks that properties with comments are separated by a blank line
- name: BLANK_LINE_BETWEEN_PROPERTIES
enabled: false
# Checks top level order
- name: TOP_LEVEL_ORDER
enabled: false
# Checks that non-empty code blocks with braces follow the K&R style (1TBS or OTBS style)
- name: BRACES_BLOCK_STRUCTURE_ERROR
enabled: false
configuration:
openBraceNewline: 'True'
closeBraceNewline: 'True'
# Checks that there is no empty blocks in a file.
# If allowEmptyBlocks is true, checks that it follows correct style (have a newline)
- name: EMPTY_BLOCK_STRUCTURE_ERROR
enabled: false
configuration:
# Whether a newline after `{` is required in an empty block
styleEmptyBlockWithNewline: 'True'
allowEmptyBlocks: 'False'
# Checks that semicolons are not used at the end of a line
- name: REDUNDANT_SEMICOLON
enabled: false
# Checks that there are not too many consecutive spaces in line
- name: TOO_MANY_CONSECUTIVE_SPACES
enabled: false
configuration:
# Maximum allowed number of consecutive spaces (not counting indentation)
maxSpaces: '1'
# Whether formatting for enums should be kept without checking
saveInitialFormattingForEnums: false
# Inspection that checks if a long dot qualified expression is used in condition or as an argument
- name: COMPLEX_EXPRESSION
enabled: false
# Checks that blank lines are used correctly.
# For example: triggers when there are too many blank lines between function declaration
- name: TOO_MANY_BLANK_LINES
enabled: false
# Checks that usage of horizontal spaces doesn't violate code style guide
- name: WRONG_WHITESPACE
enabled: false
# Checks that backticks (``) are not used in the identifier name, except the case when it is test method (marked with @Test annotation)
- name: BACKTICKS_PROHIBITED
enabled: false
# Checks that a single line concatenation of strings is not used
- name: STRING_CONCATENATION
enabled: false
# Checks that each when statement have else in the end
- name: WHEN_WITHOUT_ELSE
enabled: false
# Checks that enum structure is correct: enum entries should be separated by comma and line break and last entry should have semicolon in the end.
- name: ENUMS_SEPARATED
enabled: false
# Checks that value on integer or float constant is not too big
- name: LONG_NUMERICAL_VALUES_SEPARATED
enabled: false
configuration:
# Maximum number of digits which are not split
maxNumberLength: '5'
# Maximum number of digits between separators
maxBlockLength: '3'
# Checks magic number
- name: MAGIC_NUMBER
enabled: false
configuration:
# Ignore numbers from test
ignoreTest: "true"
# Ignore numbers
ignoreNumbers: "-1, 1, 0, 2, 0U, 1U, 2U, -1L, 0L, 1L, 2L, 0UL, 1UL, 2UL"
# Is ignore override hashCode function
ignoreHashCodeFunction: "true"
# Is ignore property
ignorePropertyDeclaration: "false"
# Is ignore local variable
ignoreLocalVariableDeclaration: "false"
# Is ignore constant
ignoreConstantDeclaration: "true"
# Is ignore property in companion object
ignoreCompanionObjectPropertyDeclaration: "true"
# Is ignore numbers in enum
ignoreEnums: "false"
# Is ignore number in ranges
ignoreRanges: "false"
# Is ignore number in extension function
ignoreExtensionFunctions: "false"
# Checks that order of enum values or constant property inside companion is correct
- name: WRONG_DECLARATIONS_ORDER
enabled: false
configuration:
# Whether enum members should be sorted alphabetically
sortEnum: true
# Whether class properties should be sorted alphabetically
sortProperty: true
# Checks that multiple modifiers sequence is in the correct order
- name: WRONG_MULTIPLE_MODIFIERS_ORDER
enabled: false
# Checks that identifier has appropriate name (See table of rule 1.2 part 6)
- name: CONFUSING_IDENTIFIER_NAMING
enabled: false
# Checks year in the copyright
- name: WRONG_COPYRIGHT_YEAR
enabled: false
# Inspection that checks if local variables are declared close to the first usage site
- name: LOCAL_VARIABLE_EARLY_DECLARATION
enabled: false
# Try to avoid initialize val by null (e.g. val a: Int? = null -> val a: Int = 0)
- name: NULLABLE_PROPERTY_TYPE
enabled: false
# Inspection that checks if there is a blank line before kDoc and none after
- name: WRONG_NEWLINES_AROUND_KDOC
enabled: false
# Inspection that checks if there is no blank lines before first comment
- name: FIRST_COMMENT_NO_BLANK_LINE
enabled: false
# Inspection that checks if there are blank lines between code and comment and between code start token and comment's text
- name: COMMENT_WHITE_SPACE
enabled: false
configuration:
maxSpacesBeforeComment: 2
maxSpacesInComment: 1
# Inspection that checks if all comment's are inside if-else code blocks. Exception is general if comment
- name: IF_ELSE_COMMENTS
enabled: false
# Type aliases provide alternative names for existing types when type's reference text is longer 25 chars
- name: TYPE_ALIAS
enabled: false
configuration:
typeReferenceLength: '25' # max length of type reference
# Checks if casting can be omitted
- name: SMART_CAST_NEEDED
enabled: false
# Checks that variables of generic types have explicit type declaration
- name: GENERIC_VARIABLE_WRONG_DECLARATION
enabled: false
# Inspection that checks if string template has redundant curly braces
- name: STRING_TEMPLATE_CURLY_BRACES
enabled: false
# Variables with `val` modifier - are immutable (read-only). Usage of such variables instead of `var` variables increases
# robustness and readability of code, because `var` variables can be reassigned several times in the business logic.
# This rule prohibits usage of `var`s as local variables - the only exception is accumulators and counters
- name: SAY_NO_TO_VAR
enabled: false
# Inspection that checks if string template has redundant quotes
- name: STRING_TEMPLATE_QUOTES
enabled: false
# Check if there are redundant nested if-statements, which could be collapsed into a single one by concatenating their conditions
- name: COLLAPSE_IF_STATEMENTS
enabled: false
configuration:
startCollapseFromNestedLevel: 2
# Checks that floating-point values are not used in arithmetic expressions
- name: FLOAT_IN_ACCURATE_CALCULATIONS
enabled: false
# Checks that function length isn't too long
- name: TOO_LONG_FUNCTION
enabled: false
configuration:
maxFunctionLength: '30' # max length of function
isIncludeHeader: 'false' # count function's header
# Warns if there are nested functions
- name: AVOID_NESTED_FUNCTIONS
enabled: false
# Checks that lambda inside function parameters is in the end
- name: LAMBDA_IS_NOT_LAST_PARAMETER
enabled: false
# Checks that function doesn't contains too many parameters
- name: TOO_MANY_PARAMETERS
enabled: false
configuration:
maxParameterListSize: '5' # max parameters size
# Checks that function doesn't have too many nested blocks
- name: NESTED_BLOCK
enabled: false
configuration:
maxNestedBlockQuantity: '4'
# Checks that function use default values, instead overloading
- name: WRONG_OVERLOADING_FUNCTION_ARGUMENTS
enabled: false
# Checks that using runBlocking inside async block code
- name: RUN_BLOCKING_INSIDE_ASYNC
enabled: false
# Checks that property in constructor doesn't contain comment
- name: KDOC_NO_CONSTRUCTOR_PROPERTY
enabled: false
# Checks that using unnecessary, custom label
- name: CUSTOM_LABEL
enabled: false
# Check that lambda with inner lambda doesn't use implicit parameter
- name: PARAMETER_NAME_IN_OUTER_LAMBDA
enabled: false
# Checks that property in KDoc present in class
- name: KDOC_EXTRA_PROPERTY
enabled: false
# There's a property in KDoc which is already present
- name: KDOC_DUPLICATE_PROPERTY
enabled: false
# Checks that KDoc in constructor has property tag but with comment inside constructor
- name: KDOC_NO_CONSTRUCTOR_PROPERTY_WITH_COMMENT
enabled: false
# if a class has single constructor, it should be converted to a primary constructor
- name: SINGLE_CONSTRUCTOR_SHOULD_BE_PRIMARY
enabled: false
# Checks if class can be made as data class
- name: USE_DATA_CLASS
enabled: false
# Checks that never use the name of a variable in the custom getter or setter
- name: WRONG_NAME_OF_VARIABLE_INSIDE_ACCESSOR
enabled: false
# Checks that classes have only one init block
- name: MULTIPLE_INIT_BLOCKS
enabled: false
# Checks that there are abstract functions in abstract class
- name: CLASS_SHOULD_NOT_BE_ABSTRACT
enabled: false
# Checks if there are any trivial getters or setters
- name: TRIVIAL_ACCESSORS_ARE_NOT_RECOMMENDED
enabled: false
# Checks that no custom getters and setters are used for properties. It is a more wide rule than TRIVIAL_ACCESSORS_ARE_NOT_RECOMMENDED
# Kotlin compiler automatically generates `get` and `set` methods for properties and also lets the possibility to override it.
# But in all cases it is very confusing when `get` and `set` are overridden for a developer who uses this particular class.
# Developer expects to get the value of the property, but receives some unknown value and some extra side effect hidden by the custom getter/setter.
# Use extra functions for it instead.
- name: CUSTOM_GETTERS_SETTERS
enabled: false
# Checks if null-check was used explicitly (for example: if (a == null))
# Try to avoid explicit null checks (explicit comparison with `null`)
# Kotlin is declared as [Null-safe](https://kotlinlang.org/docs/reference/null-safety.html) language.
# But Kotlin architects wanted Kotlin to be fully compatible with Java, that's why `null` keyword was also introduced in Kotlin.
# There are several code-structures that can be used in Kotlin to avoid null-checks. For example: `?:`, `.let {}`, `.also {}`, e.t.c
- name: AVOID_NULL_CHECKS
enabled: false
# Checks if class instantiation can be wrapped in `apply` for better readability
- name: COMPACT_OBJECT_INITIALIZATION
enabled: false
# Checks explicit supertype qualification
- name: USELESS_SUPERTYPE
enabled: false
# Checks if extension function with the same signature don't have related classes
- name: EXTENSION_FUNCTION_SAME_SIGNATURE
enabled: false
# Checks if there is empty primary constructor
- name: EMPTY_PRIMARY_CONSTRUCTOR
enabled: false
# In case of not using field keyword in property accessors,
# there should be explicit backing property with the name of real property
# Example: val table get() {if (_table == null) ...} -> table should have _table
- name: NO_CORRESPONDING_PROPERTY
enabled: false
# Checks if there is class/object that can be replace with extension function
- name: AVOID_USING_UTILITY_CLASS
enabled: false
# If there is stateless class it is preferred to use object
- name: OBJECT_IS_PREFERRED
enabled: false
# If there exists negated version of function you should prefer it instead of !functionCall
- name: INVERSE_FUNCTION_PREFERRED
enabled: false
# Checks if class can be converted to inline class
- name: INLINE_CLASS_CAN_BE_USED
enabled: false
# If file contains class, then it can't contain extension functions for the same class
- name: EXTENSION_FUNCTION_WITH_CLASS
enabled: false
# Check if kts script contains other functions except run code
- name: RUN_IN_SCRIPT
enabled: false
# Check if boolean expression can be simplified
- name: COMPLEX_BOOLEAN_EXPRESSION
enabled: false
# Check if range can replace with until or `rangeTo` function with range
- name: CONVENTIONAL_RANGE
enabled: false
configuration:
isRangeToIgnore: false
# Check if there is a call of print()\println() or console.log(). Assumption that it's a debug print
- name: DEBUG_PRINT
enabled: false
# Check that typealias name is in PascalCase
- name: TYPEALIAS_NAME_INCORRECT_CASE
enabled: false
# Should change property length - 1 to property lastIndex
- name: USE_LAST_INDEX
enabled: false
# Only properties from the primary constructor should be documented in a @property tag in class KDoc
- name: KDOC_NO_CLASS_BODY_PROPERTIES_IN_HEADER
enabled: false

View File

@ -1,4 +1,5 @@
package ru.touchin.exception.handler.spring.configurations
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.ComponentScan
import org.springframework.context.annotation.Configuration

View File

@ -33,10 +33,14 @@ class ExceptionHandlerAdvice(
val body = exceptionResponseBodyCreator(result.apiError)
val headers = if (exceptionResolverProperties.includeHeaders) HttpHeaders().apply {
set("X-Error-Code", result.apiError.errorCode.toString())
set("X-Error-Message", result.apiError.errorMessage)
} else null
val headers = if (exceptionResolverProperties.includeHeaders) {
HttpHeaders().apply {
set("X-Error-Code", result.apiError.errorCode.toString())
set("X-Error-Message", result.apiError.errorMessage)
}
} else {
null
}
return ResponseEntity(body, headers, result.status)
}

View File

@ -1,4 +1,6 @@
kotlin.code.style=official
springBootVersion=2.5.3
springDependencyManagementVersion=1.0.11.RELEASE
kotlinVersion=1.5.21
detektGradlePluginVersion=1.18.0
diktatGradlePluginVersion=1.2.5
kotlinVersion=1.6.21

View File

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@ -4,6 +4,7 @@ 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.slf4j.LoggerFactory
import ru.touchin.logger.spring.annotations.AutoLogging
import ru.touchin.logger.spring.annotations.LogValue
import ru.touchin.logger.builder.LogDataItem
@ -22,6 +23,7 @@ class LogAspect(
) {
@Around("@annotation(autoLoggingAnnotation)")
@Suppress("TooGenericExceptionCaught")
fun logInvocation(pjp: ProceedingJoinPoint, autoLoggingAnnotation: AutoLogging): Any? {
val duration = LogDuration()
@ -46,8 +48,11 @@ class LogAspect(
.build()
.error()
} catch (logError: Throwable) {
error.printStackTrace()
logError.printStackTrace()
LoggerFactory.getLogger(this::class.java)
.let { logger ->
logger.error("Cannot build logger", error)
logger.error("Cannot create logger", logError)
}
}
}

View File

@ -16,6 +16,7 @@ interface LogBuilder<T> {
fun addData(vararg items: LogDataItem): LogBuilder<T>
fun setMethod(method: String): LogBuilder<T>
fun setContext(context: LogExecutionContextData): LogBuilder<LogData>
fun setContext(): LogBuilder<LogData>
fun build(): Log<T>
fun isEnabled(logLevel: LogLevel): Boolean

View File

@ -38,10 +38,14 @@ class LogBuilderImpl(
logData.method = method
}
override fun setContext(context: LogExecutionContextData): LogBuilder<LogData> = also {
override fun setContext(): LogBuilder<LogData> = also {
logData.ctx = LoggerExecutionContext.current.get()
}
override fun setContext(context: LogExecutionContextData): LogBuilder<LogData> = also {
logData.ctx = context
}
override fun build(): Log<LogData> {
if (logData.ctx == null) {
logData.ctx = LoggerExecutionContext.current.get()

View File

@ -1,6 +1,6 @@
package ru.touchin.logger.context
@Suppress("unused", "EnumEntryName")
@Suppress("unused", "EnumEntryName", "EnumNaming")
enum class DefaultContextFields {
id,
host,

View File

@ -1,5 +1,5 @@
package ru.touchin.logger.dto
enum class LogLevel {
Trace, Info, Error
Trace, Debug, Info, Error
}

View File

@ -1,5 +1,10 @@
package ru.touchin.logger.log
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.SerializationFeature
import com.fasterxml.jackson.databind.json.JsonMapper
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import org.slf4j.LoggerFactory
import ru.touchin.logger.dto.LogLevel
import ru.touchin.logger.dto.LogData
@ -26,6 +31,14 @@ abstract class AbstractLog(clazz: Class<*>) : Log<LogData> {
}
}
override fun debug() {
if (logger.isDebugEnabled) {
val logMessage = getMessage()
logger.debug(logMessage.message, logMessage.error)
}
}
override fun info() {
if (logger.isInfoEnabled) {
val logMessage = getMessage()
@ -53,9 +66,20 @@ abstract class AbstractLog(clazz: Class<*>) : Log<LogData> {
override fun isEnabled(level: LogLevel): Boolean {
return when(level) {
LogLevel.Trace -> logger.isTraceEnabled
LogLevel.Debug -> logger.isDebugEnabled
LogLevel.Info -> logger.isInfoEnabled
LogLevel.Error -> logger.isErrorEnabled
}
}
fun objectMapper() = objectMapper
companion object {
val objectMapper: ObjectMapper = JsonMapper.builder()
.addModule(JavaTimeModule())
.build()
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
}
}

View File

@ -1,33 +1,20 @@
package ru.touchin.logger.log
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.SerializationFeature
import com.fasterxml.jackson.databind.json.JsonMapper
import com.fasterxml.jackson.databind.node.ObjectNode
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import ru.touchin.logger.dto.LogError
import ru.touchin.logger.dto.LogError.Companion.stackTraceAsString
import ru.touchin.logger.dto.LogError.Companion.toLogError
open class JsonLogImpl(clazz: Class<*>) : AbstractLog(clazz) {
companion object {
val objectMapper: ObjectMapper = JsonMapper.builder()
.addModule(JavaTimeModule())
.build()
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
}
private fun toJson(): JsonNode {
val result = objectMapper.convertValue(logData, ObjectNode::class.java)
val result = objectMapper().convertValue(logData, ObjectNode::class.java)
logData.error
?.toLogError()
?.let {
objectMapper.convertValue(it, JsonNode::class.java)
objectMapper().convertValue(it, JsonNode::class.java)
}
?.let {
result.remove("error")
@ -39,7 +26,7 @@ open class JsonLogImpl(clazz: Class<*>) : AbstractLog(clazz) {
override fun getMessage(): LogMessage {
val message = runCatching {
objectMapper.writeValueAsString(toJson())
objectMapper().writeValueAsString(toJson())
}.getOrElse { throwable ->
"""
{

View File

@ -5,8 +5,8 @@ import ru.touchin.logger.dto.LogLevel
interface Log<T> {
var logData: T
fun trace()
fun debug()
fun info()
fun error()

View File

@ -4,6 +4,7 @@ class SimpleLogImpl(clazz: Class<*>): AbstractLog(clazz) {
override fun getMessage(): LogMessage {
val builder = StringBuilder()
val pretty = objectMapper().writerWithDefaultPrettyPrinter()
builder.append("\n\ttags: ${logData.tags.joinToString(",")}")
@ -11,6 +12,18 @@ class SimpleLogImpl(clazz: Class<*>): AbstractLog(clazz) {
builder.append("\n\tduration: ${logData.duration}ms")
}
if (logData.method != null) {
builder.append("\n\tmethod: ${logData.method}")
}
if (!logData.ctx.isNullOrEmpty()) {
builder.append("\n\tcontext:\n${pretty.writeValueAsString(logData.ctx)}")
}
if (logData.data.isNotEmpty()) {
builder.append("\n\tdata:\n${pretty.writeValueAsString(logData.data)}")
}
return LogMessage(
message = builder.toString(),
error = logData.error

View File

@ -1,6 +1,9 @@
package ru.touchin.push.message.provider.fcm.configurations
import com.fasterxml.jackson.annotation.JsonAutoDetect
import com.fasterxml.jackson.annotation.PropertyAccessor
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.SerializationFeature
import com.google.auth.oauth2.AccessToken
import com.google.auth.oauth2.GoogleCredentials
import com.google.firebase.FirebaseApp
@ -25,6 +28,7 @@ class PushMessageProviderFcmConfiguration {
@Bean
fun firebaseMessaging(
properties: PushMessageProviderFcmProperties,
@Qualifier("push-message-provider.fcm.credentials-object-mapper")
objectMapper: ObjectMapper
): FirebaseMessaging {
val credentials = when {
@ -60,10 +64,22 @@ class PushMessageProviderFcmConfiguration {
return FirebaseMessaging.getInstance(firebaseApp)
}
@Bean
@Qualifier("push-message-provider.fcm.auth")
@Bean("push-message-provider.fcm.credentials-date-format")
fun simpleDateFormat(): SimpleDateFormat {
return SimpleDateFormat("yyyy-MM-dd HH:mm:ss X", Locale.getDefault())
}
@Bean("push-message-provider.fcm.credentials-object-mapper")
fun objectMapper(
@Qualifier("push-message-provider.fcm.credentials-date-format")
simpleDateFormat: SimpleDateFormat
): ObjectMapper {
return ObjectMapper().apply {
configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false)
setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE)
setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY)
dateFormat = simpleDateFormat
}
}
}

View File

@ -1,4 +1,4 @@
package ru.touchin.push.message.provider.fcm.configurations
package ru.touchin.push.message.provider.fcm.converters
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.boot.context.properties.ConfigurationPropertiesBinding
@ -10,12 +10,12 @@ import java.util.*
@ConfigurationPropertiesBinding
@Component
class DateConverter(
@Qualifier("push-message-provider.fcm.auth")
@Qualifier("push-message-provider.fcm.credentials-date-format")
private val simpleDateFormat: SimpleDateFormat
) : Converter<String, Date> {
override fun convert(source: String?): Date? {
return source?.let(simpleDateFormat::parse)
override fun convert(source: String): Date {
return simpleDateFormat.parse(source)
}
}

View File

@ -4,7 +4,7 @@ import com.google.firebase.messaging.Notification as FcmNotification
import org.springframework.stereotype.Component
import ru.touchin.push.message.provider.dto.PushMessageNotification
@Component
@Component("push-message-provider.fcm.push-message-notification-converter")
class PushMessageNotificationConverter {
operator fun invoke(pushMessageNotification: PushMessageNotification): FcmNotification {

View File

@ -9,7 +9,7 @@ import org.springframework.stereotype.Component
import ru.touchin.push.message.provider.dto.PushMessageNotification
import ru.touchin.push.message.provider.dto.request.PushTokenMessage
@Component
@Component("push-message-provider.fcm.push-token-message-converter")
class PushTokenMessageConverter(
private val pushMessageNotificationConverter: PushMessageNotificationConverter
) {

View File

@ -1,31 +1,22 @@
package ru.touchin.push.message.provider.fcm
import com.fasterxml.jackson.annotation.JsonAutoDetect
import com.fasterxml.jackson.annotation.PropertyAccessor
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.SerializationFeature
import org.springframework.beans.factory.annotation.Qualifier
import com.google.firebase.FirebaseApp
import org.springframework.boot.SpringBootConfiguration
import org.springframework.boot.test.context.TestConfiguration
import org.springframework.context.annotation.Bean
import java.text.SimpleDateFormat
import org.springframework.context.ApplicationListener
import org.springframework.context.event.ContextRefreshedEvent
@TestConfiguration
@SpringBootConfiguration
@EnablePushMessageProviderFcm
class PushMessageProviderFcmTestApplication {
class PushMessageProviderFcmTestApplication : ApplicationListener<ContextRefreshedEvent> {
@Bean
fun objectMapper(
@Qualifier("push-message-provider.fcm.auth")
simpleDateFormat: SimpleDateFormat
): ObjectMapper {
return ObjectMapper().apply {
configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false)
setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE)
setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY)
dateFormat = simpleDateFormat
}
override fun onApplicationEvent(event: ContextRefreshedEvent) {
clearSingletonsOutsideContainer()
}
private fun clearSingletonsOutsideContainer() {
FirebaseApp.getApps().forEach(FirebaseApp::delete)
}
}

View File

@ -7,7 +7,10 @@ internal open class ConditionalResponse<S, F>(
init {
// Only one value should be present
check((success == null) != (failure == null))
val hasSuccessValue = success != null
val hasFailureValue = failure != null
check(hasSuccessValue != hasFailureValue)
}
val isSuccess: Boolean = success != null

View File

@ -8,7 +8,7 @@ internal enum class HmsResponseCode(
) : ValueableSerializableEnum<Int> {
UNKNOWN(-1, "Unknown"),
INVALID_CLIENT_SECRET(1101, "Invalid client_secret"),
INVALID_CLIENT_SECRET(1101, "Invalid client_secret: app or server has mismatching credentials"),
SUCCESS(80000000, "Success"),
SOME_TOKENS_ARE_INVALID(80100000, "Some tokens are right, the others are illegal"),
PARAMETERS_ARE_INVALID(80100001, "Parameters check error"),

View File

@ -10,20 +10,19 @@ import org.springframework.web.reactive.function.client.WebClient
import ru.touchin.common.spring.web.webclient.dto.RequestLogData
import ru.touchin.common.spring.web.webclient.logger.WebClientLogger
import ru.touchin.push.message.provider.hpk.base.clients.ConfigurableWebClient
import ru.touchin.push.message.provider.hpk.clients.hms_hpk.bodies.HmsHpkMessagesSendBody
import ru.touchin.push.message.provider.hpk.clients.hms_hpk.requests.HmsHpkMessagesSendRequest
import ru.touchin.push.message.provider.hpk.clients.hms_hpk.responses.HmsHpkResponse
import ru.touchin.push.message.provider.hpk.properties.HpkProperties
private const val METHOD_MESSAGES_SEND = "messages:send"
/**
* Client for Huawei Push Kit.
* @see <a href="https://developer.huawei.com/consumer/en/doc/development/HMSCore-References/https-send-api-0000001050986197">Documentation</a>
*/
@Suppress("LongLine")
@Component
class HmsHpkWebClient(
webClientLogger: WebClientLogger,
@Qualifier("push-message-provider.hpk.hms-hpk-webclient-builder")
webClientBuilder: WebClient.Builder,
private val hpkProperties: HpkProperties,
@Qualifier("push-message-provider.hpk.webclient-objectmapper")
@ -63,4 +62,10 @@ class HmsHpkWebClient(
.block() ?: throw IllegalStateException("No response")
}
private companion object {
const val METHOD_MESSAGES_SEND = "messages:send"
}
}

View File

@ -18,7 +18,9 @@ internal data class AndroidConfig private constructor(
*
* 1-100: message cache group ID. Messages are cached by group. Each group can cache only one message for each app.
*
* For example, if you send 10 messages and set collapse_key to 1 for the first five messages and to 2 for the rest, the latest message whose value of collapse_key is 1 and the latest message whose value of collapse_key is 2 are sent to the user after the user's device goes online.
* For example, if you send 10 messages and set collapse_key to 1 for the first five messages and to 2 for the rest,
* the latest message whose value of collapse_key is 1 and
* the latest message whose value of collapse_key is 2 are sent to the user after the user's device goes online.
*
* The default value is -1.
* */
@ -54,7 +56,10 @@ internal data class AndroidConfig private constructor(
fun check(androidConfig: AndroidConfig, notification: Notification?) {
with(androidConfig) {
if (collapseKey != null) {
require(collapseKey in -1..100) { "Collapse Key should be [-1, 100]" }
require(
collapseKey in COLLAPSE_KEY_RANGE_CONSTRAINT
)
{ "Collapse Key must be in $COLLAPSE_KEY_MIN_VALUE and $COLLAPSE_KEY_MAX_VALUE" }
}
if (ttl != null) {
require(ttl.matches(TTL_PATTERN)) { "The TTL's format is wrong" }
@ -67,6 +72,9 @@ internal data class AndroidConfig private constructor(
private companion object {
const val COLLAPSE_KEY_MIN_VALUE: Byte = -1
const val COLLAPSE_KEY_MAX_VALUE: Byte = 100
val COLLAPSE_KEY_RANGE_CONSTRAINT: IntRange = COLLAPSE_KEY_MIN_VALUE..COLLAPSE_KEY_MAX_VALUE
val TTL_PATTERN: Regex = Regex("\\d+|\\d+[sS]|\\d+.\\d{1,9}|\\d+.\\d{1,9}[sS]")
}

View File

@ -11,11 +11,11 @@ internal data class Message private constructor(
val notification: Notification?,
/** Android message push control. */
@JsonProperty("android")
val android: AndroidConfig?,
val androidConfig: AndroidConfig?,
@JsonProperty("apns")
val apns: ApnsConfig?,
val apnsConfig: ApnsConfig?,
@JsonProperty("webpush")
val webpush: WebPushConfig?,
val webPushConfig: WebPushConfig?,
/** Push token of the target user of a message. */
val token: Collection<String>?,
/** Topic subscribed by the target user of a message. */
@ -32,22 +32,31 @@ internal data class Message private constructor(
!token.isNullOrEmpty(),
!topic.isNullOrBlank(),
!condition.isNullOrBlank(),
).count { it } == 1
).count { it } == MAX_TYPES_OF_DELIVERY
) { "Exactly one of token, topic or condition must be specified" }
if (token != null) {
require(
token.size in 1..1000
) { "Number of tokens, if specified, must be from 1 to 1000" }
token.size in TOKENS_SIZE_RANGE_CONSTRAINT
) { "Number of tokens, if specified, must be from $TOKENS_SIZE_MIN to $TOKENS_SIZE_MAX" }
}
notification?.also { Notification.validator.check(it) }
android?.also { AndroidConfig.validator.check(it, notification) }
apns?.also { ApnsConfig.validator.check(it) }
webpush?.also { WebPushConfig.validator.check(it) }
androidConfig?.also { AndroidConfig.validator.check(it, notification) }
apnsConfig?.also { ApnsConfig.validator.check(it) }
webPushConfig?.also { WebPushConfig.validator.check(it) }
}
}
private companion object {
const val MAX_TYPES_OF_DELIVERY: Int = 1
const val TOKENS_SIZE_MIN: Byte = 1
const val TOKENS_SIZE_MAX: Short = 1000
val TOKENS_SIZE_RANGE_CONSTRAINT: IntRange = TOKENS_SIZE_MIN..TOKENS_SIZE_MAX
}
}
class Builder : Buildable {
@ -55,8 +64,8 @@ internal data class Message private constructor(
private var data: String? = null
private var notification: Notification? = null
private var androidConfig: AndroidConfig? = null
private var apns: ApnsConfig? = null
private var webpush: WebPushConfig? = null
private var apnsConfig: ApnsConfig? = null
private var webPushConfig: WebPushConfig? = null
private val token: MutableList<String> = mutableListOf()
private var topic: String? = null
private var condition: String? = null
@ -76,13 +85,13 @@ internal data class Message private constructor(
return this
}
fun setApns(apns: ApnsConfig): Builder {
this.apns = apns
fun setApns(apnsConfig: ApnsConfig): Builder {
this.apnsConfig = apnsConfig
return this
}
fun setWebpush(webpush: WebPushConfig): Builder {
this.webpush = webpush
fun setWebpush(webPushConfig: WebPushConfig): Builder {
this.webPushConfig = webPushConfig
return this
}
@ -105,9 +114,9 @@ internal data class Message private constructor(
return Message(
data = data,
notification = notification,
android = androidConfig,
apns = apns,
webpush = webpush,
androidConfig = androidConfig,
apnsConfig = apnsConfig,
webPushConfig = webPushConfig,
topic = topic,
condition = condition,
token = token.takeIf(Collection<*>::isNotEmpty),

View File

@ -17,15 +17,15 @@ internal data class Notification private constructor(
with(notification) {
if (image != null) {
require(
image.matches(HTTPS_URL_PATTERN)
) { "image's url should start with HTTPS" }
image.startsWith(HTTPS_URL_PATTERN)
) { "image's url should start with $HTTPS_URL_PATTERN" }
}
}
}
private companion object {
val HTTPS_URL_PATTERN: Regex = Regex("^https.*")
const val HTTPS_URL_PATTERN: String = "https"
}

View File

@ -30,16 +30,36 @@ internal data class WebPushConfig private constructor(
class Builder : Buildable {
private var headers: WebPushHeaders? = null
private var webPushHeaders: WebPushHeaders? = null
private var data: String? = null
private var notification: WebNotification? = null
private var webNotification: WebNotification? = null
private var webHmsOptions: WebHmsOptions? = null
fun setHeaders(webPushHeaders: WebPushHeaders): Builder {
this.webPushHeaders = webPushHeaders
return this
}
fun setData(data: String): Builder {
this.data = data
return this
}
fun setNotification(webNotification: WebNotification): Builder {
this.webNotification = webNotification
return this
}
fun setWebHmsOptions(webHmsOptions: WebHmsOptions): Builder {
this.webHmsOptions = webHmsOptions
return this
}
fun build(): WebPushConfig {
return WebPushConfig(
webPushHeaders = headers,
webPushHeaders = webPushHeaders,
data = data,
webNotification = notification,
webNotification = webNotification,
webHmsOptions = webHmsOptions,
)
}

View File

@ -19,18 +19,29 @@ internal data class AndroidBadgeNotification private constructor(
with(androidBadgeNotification) {
if (addNum != null) {
require(
addNum in 1..99
) { "add_num must locate between 0 and 100" }
addNum in ADD_NUM_MIN_VALUE..ADD_NUM_MAX_VALUE
) { "add_num must locate in $ADD_NUM_MIN_VALUE and $ADD_NUM_MAX_VALUE" }
}
if (setNum != null) {
require(
setNum in 0..99
) { "set_num must locate between -1 and 100" }
setNum in SET_NUM_RANGE_CONSTRAINT
) { "set_num must locate between $SET_NUM_MIN_VALUE and $SET_NUM_MAX_VALUE" }
}
}
}
private companion object {
const val ADD_NUM_MIN_VALUE: Byte = 1
const val ADD_NUM_MAX_VALUE: Byte = 99
val ADD_NUM_RANGE_CONSTRAINT: IntRange = ADD_NUM_MIN_VALUE..ADD_NUM_MAX_VALUE
const val SET_NUM_MIN_VALUE: Byte = 0
const val SET_NUM_MAX_VALUE: Byte = 99
val SET_NUM_RANGE_CONSTRAINT: IntRange = SET_NUM_MIN_VALUE..SET_NUM_MAX_VALUE
}
}
class Builder : Buildable {

View File

@ -24,16 +24,23 @@ internal data class AndroidButton private constructor(
fun check(androidButton: AndroidButton) {
with(androidButton) {
require(
name.length < 40
) { "Button name length cannot exceed 40" }
name.length <= NAME_MAX_LENGTH
) { "Button name length cannot exceed $NAME_MAX_LENGTH" }
if (androidActionType == AndroidActionType.SHARE_NOTIFICATION_MESSAGE) {
require(!data.isNullOrEmpty()) { "Data is needed when actionType is $androidActionType" }
require(data.length < 1024) { "Data length cannot exceed 1024 chars" }
require(data.length <= DATA_MAX_LENGTH) { "Data length cannot exceed $DATA_MAX_LENGTH chars" }
}
}
}
private companion object {
const val NAME_MAX_LENGTH: Byte = 40
const val DATA_MAX_LENGTH: Short = 1024
}
}
class Builder : Buildable {
@ -42,7 +49,7 @@ internal data class AndroidButton private constructor(
private var data: String? = null
fun setIntent(intent: String): Builder {
this.intent
this.intent = intent
return this
}

View File

@ -26,7 +26,7 @@ internal data class AndroidClickAction private constructor(
AndroidClickActionType.OPEN_URL -> {
require(!url.isNullOrBlank()) { "url is required when click type is $androidClickActionType" }
require(url.matches(HTTPS_PATTERN)) { "url must start with https" }
require(url.startsWith(HTTPS_PATTERN_START)) { "url must start with $HTTPS_PATTERN_START" }
}
AndroidClickActionType.OPEN_APP -> {
@ -38,7 +38,7 @@ internal data class AndroidClickAction private constructor(
private companion object {
val HTTPS_PATTERN: Regex = Regex("^https.*")
const val HTTPS_PATTERN_START = "https"
}

View File

@ -17,17 +17,18 @@ internal data class AndroidColor private constructor(
fun check(androidColor: AndroidColor) {
with(androidColor) {
require(alpha > ZERO && alpha < ONE) { "Alpha must be locate between [0,1]" }
require(red > ZERO && red < ONE) { "Red must be locate between [0,1]" }
require(green > ZERO && green < ONE) { "Green must be locate between [0,1]" }
require(blue > ZERO && blue < ONE) { "Blue must be locate between [0,1]" }
require(alpha in COLOR_RANGE_CONSTRAINT) { "Alpha must be locate between [0,1]" }
require(red in COLOR_RANGE_CONSTRAINT) { "Red must be locate between [0,1]" }
require(green in COLOR_RANGE_CONSTRAINT) { "Green must be locate between [0,1]" }
require(blue in COLOR_RANGE_CONSTRAINT) { "Blue must be locate between [0,1]" }
}
}
private companion object {
private const val ZERO: Float = -0.000001f
private const val ONE: Float = 1.000001f
private const val ZERO: Float = 0.0f
private const val ONE: Float = 1.0f
val COLOR_RANGE_CONSTRAINT = ZERO..ONE
}

View File

@ -21,10 +21,10 @@ internal data class AndroidLightSettings private constructor(
require(
lightOnDuration.matches(LIGHT_DURATION_PATTERN)
) { "light_on_duration format is wrong" }
) { "light_on_duration pattern is wrong" }
require(
lightOffDuration.matches(LIGHT_DURATION_PATTERN)
) { "light_off_duration format is wrong" }
) { "light_off_duration pattern is wrong" }
}
}

View File

@ -106,6 +106,7 @@ internal data class AndroidNotificationConfig private constructor(
class Validator {
@Suppress("ComplexMethod")
fun check(androidNotificationConfig: AndroidNotificationConfig, notification: Notification?) {
with(androidNotificationConfig) {
androidBadgeNotification?.let { AndroidBadgeNotification.validator.check(it) }
@ -118,9 +119,11 @@ internal data class AndroidNotificationConfig private constructor(
if (!color.isNullOrBlank()) {
require(color.matches(COLOR_PATTERN)) { "Wrong color format, color must be in the form #RRGGBB" }
}
if (!image.isNullOrBlank()) {
require(image.matches(HTTPS_URL_PATTERN)) { "notifyIcon must start with https" }
require(image.startsWith(HTTPS_URL_PATTERN)) { "notifyIcon must start with $HTTPS_URL_PATTERN" }
}
if (androidStyleType != null) {
when (androidStyleType) {
AndroidStyleType.DEFAULT -> {
@ -135,13 +138,20 @@ internal data class AndroidNotificationConfig private constructor(
AndroidStyleType.INBOX -> {
require(
!inboxContent.isNullOrEmpty() && inboxContent.orEmpty().size <= 5
) { "inboxContent is required when style is $androidStyleType and at most 5 inbox content allowed" }
!inboxContent.isNullOrEmpty()
) { "inboxContent is required when style is $androidStyleType" }
require(
inboxContent.size <= INBOX_CONTENT_MAX_ITEMS
) { "inboxContent must have at most $INBOX_CONTENT_MAX_ITEMS items" }
}
}
}
if (profileId != null) {
require(profileId.length <= 64) { "profileId length cannot exceed 64 characters" }
require(
profileId.length <= PROFILE_ID_MAX_LENGTH
) { "profileId length cannot exceed $PROFILE_ID_MAX_LENGTH characters" }
}
}
}
@ -149,16 +159,19 @@ internal data class AndroidNotificationConfig private constructor(
private companion object {
val COLOR_PATTERN: Regex = Regex("^#[0-9a-fA-F]{6}$")
val HTTPS_URL_PATTERN: Regex = Regex("^https.*")
const val HTTPS_URL_PATTERN: String = "https"
const val INBOX_CONTENT_MAX_ITEMS: Byte = 5
const val PROFILE_ID_MAX_LENGTH: Byte = 64
}
}
@Suppress("TooManyFunctions")
class Builder : Buildable {
private var title: String? = null
private val body: String? = null
private var body: String? = null
private var icon: String? = null
private var color: String? = null
private var sound: String? = null
@ -188,7 +201,7 @@ internal data class AndroidNotificationConfig private constructor(
private var androidLightSettings: AndroidLightSettings? = null
private var foregroundShow = false
private val inboxContent: MutableList<String> = mutableListOf()
private val buttons: MutableList<AndroidButton> = mutableListOf()
private val androidButtons: MutableList<AndroidButton> = mutableListOf()
private var profileId: String? = null
fun setTitle(title: String): Builder {
@ -197,7 +210,7 @@ internal data class AndroidNotificationConfig private constructor(
}
fun setBody(body: String): Builder {
this.body
this.body = body
return this
}
@ -346,8 +359,8 @@ internal data class AndroidNotificationConfig private constructor(
return this
}
fun addButton(vararg button: AndroidButton): Builder {
buttons.addAll(button)
fun addButton(vararg androidButton: AndroidButton): Builder {
androidButtons.addAll(androidButton)
return this
}
@ -392,7 +405,7 @@ internal data class AndroidNotificationConfig private constructor(
androidLightSettings = androidLightSettings,
foregroundShow = foregroundShow,
inboxContent = inboxContent.takeIf(Collection<*>::isNotEmpty),
androidButtons = buttons.takeIf(Collection<*>::isNotEmpty),
androidButtons = androidButtons.takeIf(Collection<*>::isNotEmpty),
profileId = profileId,
)
}

View File

@ -57,13 +57,8 @@ internal data class ApnsAlert private constructor(
return this
}
fun setAddAllTitleLocArgs(titleLocArgs: Collection<String>): Builder {
this.titleLocArgs.addAll(titleLocArgs)
return this
}
fun setAddTitleLocArg(titleLocArg: String): Builder {
titleLocArgs.add(titleLocArg)
fun setAddTitleLocArg(vararg titleLocArg: String): Builder {
titleLocArgs.addAll(titleLocArg)
return this
}
@ -77,16 +72,11 @@ internal data class ApnsAlert private constructor(
return this
}
fun addAllLocArgs(locArgs: Collection<String>): Builder {
fun addAllLocArgs(vararg locArgs: String): Builder {
this.locArgs.addAll(locArgs)
return this
}
fun addLocArg(locArg: String): Builder {
locArgs.add(locArg)
return this
}
fun setLaunchImage(launchImage: String): Builder {
this.launchImage = launchImage
return this

View File

@ -23,7 +23,7 @@ internal data class ApnsHeaders private constructor(
with(apnsHeaders) {
if (authorization != null) {
require(
authorization.matches(AUTHORIZATION_PATTERN)
authorization.startsWith(AUTHORIZATION_PATTERN)
) { "authorization must start with bearer" }
}
if (apnsId != null) {
@ -31,16 +31,17 @@ internal data class ApnsHeaders private constructor(
}
if (apnsCollapseId != null) {
require(
apnsCollapseId.toByteArray().size < 64
) { "Number of apnsCollapseId bytes should be less than 64" }
apnsCollapseId.toByteArray().size < APNS_COLLAPSE_ID_MAX_SIZE
) { "Number of apnsCollapseId bytes must be less than $APNS_COLLAPSE_ID_MAX_SIZE" }
}
}
}
private companion object {
private val AUTHORIZATION_PATTERN: Regex = Regex("^bearer*")
private val APN_ID_PATTERN: Regex = Regex("[0-9a-z]{8}(-[0-9a-z]{4}){3}-[0-9a-z]{12}")
const val AUTHORIZATION_PATTERN: String = "bearer"
val APN_ID_PATTERN: Regex = Regex("[0-9a-z]{8}(-[0-9a-z]{4}){3}-[0-9a-z]{12}")
const val APNS_COLLAPSE_ID_MAX_SIZE: Byte = 64
}

View File

@ -11,6 +11,7 @@ internal data class ApnsHmsOptions private constructor(
class Validator {
@Suppress("UNUSED_PARAMETER")
fun check(apnsHmsOptions: ApnsHmsOptions) {
// no validation
}

View File

@ -8,7 +8,7 @@ import ru.touchin.push.message.provider.hpk.base.builders.Buildable
@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy::class)
internal class Aps private constructor(
@JsonProperty("alert")
val alert: ApnsAlert?,
val apnsAlert: ApnsAlert?,
val badge: Int?,
val sound: String?,
val contentAvailable: Int?,
@ -20,8 +20,8 @@ internal class Aps private constructor(
fun check(aps: Aps) {
with(aps) {
if (alert != null) {
ApnsAlert.validator.check(alert)
if (apnsAlert != null) {
ApnsAlert.validator.check(apnsAlert)
}
}
}
@ -30,7 +30,7 @@ internal class Aps private constructor(
class Builder : Buildable {
private var alert: ApnsAlert? = null
private var apnsAlert: ApnsAlert? = null
private var badge: Int? = null
private var sound: String? = null
private var contentAvailable: Int? = null
@ -38,7 +38,7 @@ internal class Aps private constructor(
private var threadId: String? = null
fun setAlert(alert: ApnsAlert): Builder {
this.alert = alert
this.apnsAlert = alert
return this
}
@ -69,7 +69,7 @@ internal class Aps private constructor(
fun build(): Aps {
return Aps(
alert = alert,
apnsAlert = apnsAlert,
badge = badge,
sound = sound,
contentAvailable = contentAvailable,

View File

@ -22,6 +22,21 @@ internal data class WebActions private constructor(
private var icon: String? = null
private var title: String? = null
fun setAction(action: String): Builder {
this.action = action
return this
}
fun setIcon(icon: String): Builder {
this.icon = icon
return this
}
fun setTitle(title: String): Builder {
this.title = title
return this
}
fun build(): WebActions {
return WebActions(
action = action,

View File

@ -28,6 +28,11 @@ internal data class WebHmsOptions private constructor(
private var link: String? = null
fun setLink(link: String): Builder {
this.link = link
return this
}
fun build(): WebHmsOptions {
return WebHmsOptions(
link = link,

View File

@ -2,6 +2,7 @@ package ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.web
import com.fasterxml.jackson.annotation.JsonProperty
import ru.touchin.push.message.provider.hpk.base.builders.Buildable
import ru.touchin.push.message.provider.hpk.clients.hms_hpk.enums.web.WebDir
import java.util.*
internal data class WebNotification private constructor(
@ -12,32 +13,22 @@ internal data class WebNotification private constructor(
val lang: String?,
val tag: String?,
val badge: String?,
val dir: String?,
@JsonProperty("dir")
val webDir: WebDir?,
val vibrate: Collection<Int>?,
val renotify: Boolean,
val requireInteraction: Boolean,
val silent: Boolean,
val timestamp: Long?,
@JsonProperty("actions")
val actions: Collection<WebActions>?,
val webActions: Collection<WebActions>?,
) {
class Validator {
@Suppress("UNUSED_PARAMETER")
fun check(webNotification: WebNotification) {
with(webNotification) {
if (dir != null) {
require(
DIR_VALUE.any { it == dir }
) { "Invalid dir" }
}
}
}
private companion object {
val DIR_VALUE: Array<String> = arrayOf("auto", "ltr", "rtl")
// no verification required
}
}
@ -51,13 +42,83 @@ internal data class WebNotification private constructor(
private var lang: String? = null
private var tag: String? = null
private var badge: String? = null
private var dir: String? = null
private var webDir: WebDir? = null
private val vibrate: MutableList<Int> = mutableListOf()
private var renotify = false
private var requireInteraction = false
private var silent = false
private var timestamp: Long? = null
private val actions: MutableList<WebActions> = mutableListOf()
private val webActions: MutableList<WebActions> = mutableListOf()
fun setTitle(title: String): Builder {
this.title = title
return this
}
fun setBody(body: String): Builder {
this.body = body
return this
}
fun setIcon(icon: String): Builder {
this.icon = icon
return this
}
fun setImage(image: String): Builder {
this.image = image
return this
}
fun setLang(lang: String): Builder {
this.lang = lang
return this
}
fun setTag(tag: String): Builder {
this.tag = tag
return this
}
fun setBadge(badge: String): Builder {
this.badge = badge
return this
}
fun setDir(webDir: WebDir): Builder {
this.webDir = webDir
return this
}
fun addVibrate(vibrate: Int): Builder {
this.vibrate.add(vibrate)
return this
}
fun setRenotify(renotify: Boolean): Builder {
this.renotify = renotify
return this
}
fun setRequireInteraction(requireInteraction: Boolean): Builder {
this.requireInteraction = requireInteraction
return this
}
fun setSilent(silent: Boolean): Builder {
this.silent = silent
return this
}
fun setTimestamp(timestamp: Long): Builder {
this.timestamp = timestamp
return this
}
fun addActions(vararg webActions: WebActions): Builder {
this.webActions.addAll(webActions)
return this
}
fun build(): WebNotification {
return WebNotification(
@ -68,13 +129,13 @@ internal data class WebNotification private constructor(
lang = lang,
tag = tag,
badge = badge,
dir = dir,
webDir = webDir,
vibrate = vibrate.takeIf(Collection<*>::isNotEmpty),
renotify = renotify,
requireInteraction = requireInteraction,
silent = silent,
timestamp = timestamp,
actions = actions.takeIf(Collection<*>::isNotEmpty),
webActions = webActions.takeIf(Collection<*>::isNotEmpty),
)
}
}

View File

@ -1,11 +1,14 @@
package ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.web
import com.fasterxml.jackson.annotation.JsonProperty
import ru.touchin.push.message.provider.hpk.base.builders.Buildable
import ru.touchin.push.message.provider.hpk.clients.hms_hpk.enums.web.WebUrgency
internal data class WebPushHeaders private constructor(
val ttl: String?,
val topic: String?,
val urgency: String?,
@JsonProperty("urgency")
val webUrgency: WebUrgency?,
) {
class Validator {
@ -15,18 +18,12 @@ internal data class WebPushHeaders private constructor(
if (ttl != null) {
require(ttl.matches(TTL_PATTERN)) { "Invalid ttl format" }
}
if (urgency != null) {
require(
URGENCY_VALUE.all { it == urgency }
) { "Invalid urgency" }
}
}
}
private companion object {
val TTL_PATTERN: Regex = Regex("[0-9]+|[0-9]+[sS]")
val URGENCY_VALUE: Array<String> = arrayOf("very-low", "low", "normal", "high")
}
@ -36,7 +33,7 @@ internal data class WebPushHeaders private constructor(
private var ttl: String? = null
private var topic: String? = null
private var urgency: String? = null
private var urgency: WebUrgency? = null
fun setTtl(ttl: String): Builder {
this.ttl = ttl
@ -48,7 +45,7 @@ internal data class WebPushHeaders private constructor(
return this
}
fun setUrgency(urgency: String): Builder {
fun setUrgency(urgency: WebUrgency): Builder {
this.urgency = urgency
return this
}
@ -57,7 +54,7 @@ internal data class WebPushHeaders private constructor(
return WebPushHeaders(
ttl = ttl,
topic = topic,
urgency = urgency,
webUrgency = urgency,
)
}
}

View File

@ -0,0 +1,13 @@
package ru.touchin.push.message.provider.hpk.clients.hms_hpk.enums.web
import ru.touchin.push.message.provider.hpk.base.enums.ValueableSerializableEnum
enum class WebDir(
override val value: String
) : ValueableSerializableEnum<String> {
AUTO("auto"),
RTL("rtl"),
LTR("ltr"),
}

View File

@ -0,0 +1,14 @@
package ru.touchin.push.message.provider.hpk.clients.hms_hpk.enums.web
import ru.touchin.push.message.provider.hpk.base.enums.ValueableSerializableEnum
enum class WebUrgency(
override val value: String
) : ValueableSerializableEnum<String> {
VERY_LOW("very-low"),
LOW("low"),
NORMAL("normal"),
HIGH("high"),
}

View File

@ -15,12 +15,14 @@ import ru.touchin.push.message.provider.hpk.clients.hms_oauth.response.HmsOauthE
import ru.touchin.push.message.provider.hpk.clients.hms_oauth.response.HmsOauthTokenResponse
import ru.touchin.push.message.provider.hpk.properties.HpkProperties
private const val GRANT_TYPE_CLIENT_CREDENTIALS = "client_credentials"
private const val METHOD_TOKEN = "token"
/**
* Client for Huawei Oauth service.
* @see <a href="https://developer.huawei.com/consumer/en/doc/development/HMSCore-Guides/open-platform-oauth-0000001053629189">Documentation</a>
*/
@Component
class HmsOauthWebClient(
webClientLogger: WebClientLogger,
@Qualifier("push-message-provider.hpk.hms-oauth-webclient-builder")
webClientBuilder: WebClient.Builder,
private val hpkProperties: HpkProperties,
@Qualifier("push-message-provider.hpk.webclient-objectmapper")
@ -48,9 +50,9 @@ class HmsOauthWebClient(
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body(
BodyInserters
.fromFormData("grant_type", GRANT_TYPE_CLIENT_CREDENTIALS)
.with("client_id", hpkProperties.webServices.clientId)
.with("client_secret", hpkProperties.webServices.oauth.clientSecret)
.fromFormData(TOKEN_KEY_GRANT_TYPE, GRANT_TYPE_CLIENT_CREDENTIALS)
.with(TOKEN_KEY_CLIENT_ID, hpkProperties.webServices.clientId)
.with(TOKEN_KEY_CLIENT_SECRET, hpkProperties.webServices.oauth.clientSecret)
)
.exchangeWithWrap<HmsOauthTokenResponse, HmsOauthErrorResponse>(
requestLogData = RequestLogData(
@ -62,4 +64,15 @@ class HmsOauthWebClient(
.block() ?: throw IllegalStateException("No response")
}
private companion object {
const val GRANT_TYPE_CLIENT_CREDENTIALS = "client_credentials"
const val METHOD_TOKEN = "token"
const val TOKEN_KEY_GRANT_TYPE = "grant_type"
const val TOKEN_KEY_CLIENT_ID = "client_id"
const val TOKEN_KEY_CLIENT_SECRET = "client_secret"
}
}

View File

@ -6,7 +6,6 @@ import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.PropertyNamingStrategies
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.context.properties.ConfigurationPropertiesScan
import org.springframework.cache.CacheManager
@ -15,6 +14,7 @@ import org.springframework.cache.support.SimpleCacheManager
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.ComponentScan
import org.springframework.context.annotation.Import
import org.springframework.web.reactive.function.client.WebClient
import ru.touchin.push.message.provider.configurations.PushMessageProviderConfiguration
import ru.touchin.push.message.provider.hpk.services.HmsOauthAccessTokenCacheServiceImpl.Companion.HMS_CLIENT_SERVICE_CACHE_KEY
@ -23,8 +23,7 @@ import ru.touchin.push.message.provider.hpk.services.HmsOauthAccessTokenCacheSer
@Import(value = [PushMessageProviderConfiguration::class])
class PushMessageProviderHpkConfiguration {
@Bean
@Qualifier("push-message-provider.hpk.webclient-objectmapper")
@Bean("push-message-provider.hpk.webclient-objectmapper")
fun webclientObjectMapper(): ObjectMapper {
return jacksonObjectMapper()
.registerModule(JavaTimeModule())
@ -34,8 +33,7 @@ class PushMessageProviderHpkConfiguration {
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
}
@Bean
@Qualifier("push-message-provider.hpk.client-objectmapper")
@Bean("push-message-provider.hpk.client-objectmapper")
fun clientObjectMapper(): ObjectMapper {
return jacksonObjectMapper()
.registerModule(JavaTimeModule())
@ -43,9 +41,8 @@ class PushMessageProviderHpkConfiguration {
.setSerializationInclusion(JsonInclude.Include.NON_EMPTY)
}
@Bean
@Bean("push-message-provider.hpk.webclient-cachemanager")
@ConditionalOnMissingBean
@Qualifier("push-message-provider.hpk.webclient-cachemanager")
fun cacheManager(): CacheManager {
return SimpleCacheManager().also {
it.setCaches(
@ -56,4 +53,10 @@ class PushMessageProviderHpkConfiguration {
}
}
@Bean("push-message-provider.hpk.hms-oauth-webclient-builder")
fun hmsOauthWebClientBuilder(): WebClient.Builder = WebClient.builder()
@Bean("push-message-provider.hpk.hms-hpk-webclient-builder")
fun hmsHpkWebClientBuilder(): WebClient.Builder = WebClient.builder()
}

View File

@ -5,8 +5,8 @@ import ru.touchin.push.message.provider.dto.PushMessageNotification
import ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.Notification as HmsNotification
import ru.touchin.push.message.provider.hpk.base.extensions.ifNotNull
@Component
class NotificationConverter {
@Component("push-message-provider.hpk.push-message-notification-converter")
class PushMessageNotificationConverter {
internal operator fun invoke(pushMessageNotification: PushMessageNotification): HmsNotification {
return HmsNotification.builder()

View File

@ -12,9 +12,9 @@ import ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.android.AndroidN
import ru.touchin.push.message.provider.hpk.clients.hms_hpk.enums.android.AndroidClickActionType
import kotlin.jvm.Throws
@Component
@Component("push-message-provider.hpk.push-token-message-converter")
class PushTokenMessageConverter(
private val notificationConverter: NotificationConverter,
private val pushMessageNotificationConverter: PushMessageNotificationConverter,
@Qualifier("push-message-provider.hpk.client-objectmapper")
private val objectMapper: ObjectMapper,
) {
@ -27,7 +27,7 @@ class PushTokenMessageConverter(
setData(objectMapper.writeValueAsString(data))
}
.ifNotNull(request.pushMessageNotification) { notification ->
setNotification(notificationConverter(notification))
setNotification(pushMessageNotificationConverter(notification))
}
.setupAndroidConfig()
.build()

View File

@ -56,8 +56,12 @@ class HmsHpkClientServiceImpl(
)
when (HmsResponseCode.fromCode(result.code)) {
HmsResponseCode.SUCCESS -> {
// pass result
}
HmsResponseCode.INVALID_TOKEN,
HmsResponseCode.PERMISSION_DENIED -> {
HmsResponseCode.INVALID_CLIENT_SECRET -> {
throw InvalidPushTokenException()
}

View File

@ -6,12 +6,15 @@ import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.cache.Cache
import org.springframework.cache.CacheManager
import org.springframework.stereotype.Service
import ru.touchin.logger.dto.LogData
import ru.touchin.logger.factory.LogBuilderFactory
import ru.touchin.push.message.provider.hpk.properties.HpkProperties
import ru.touchin.push.message.provider.hpk.services.dto.AccessToken
import java.time.Instant
@Service
class HmsOauthAccessTokenCacheServiceImpl(
private val logBuilderFactory: LogBuilderFactory<LogData>,
@Qualifier("push-message-provider.hpk.webclient-cachemanager")
private val cacheManager: CacheManager,
@Qualifier("push-message-provider.hpk.client-objectmapper")
@ -47,7 +50,12 @@ class HmsOauthAccessTokenCacheServiceImpl(
return try {
objectMapper.convertValue(item, typeReference)
} catch (e: Exception) {
print(e.message)
logBuilderFactory.create(this::class.java)
.setMethod("safeCast")
.setError(e)
.build()
.error()
null
}
}

View File

@ -2,9 +2,7 @@ package ru.touchin.push.message.provider.hpk
import org.springframework.boot.SpringBootConfiguration
import org.springframework.boot.test.context.TestConfiguration
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Import
import org.springframework.web.reactive.function.client.WebClient
import ru.touchin.logger.spring.configurations.SpringLoggerConfiguration
import ru.touchin.logger.spring.web.configurations.SpringLoggerWebConfiguration
@ -15,9 +13,4 @@ import ru.touchin.logger.spring.web.configurations.SpringLoggerWebConfiguration
SpringLoggerConfiguration::class,
SpringLoggerWebConfiguration::class,
)
class PushMessageProviderHpkTestApplication {
@Bean
fun webClientBuilder(): WebClient.Builder = WebClient.builder()
}
class PushMessageProviderHpkTestApplication

View File

@ -0,0 +1,33 @@
package ru.touchin.push.message.provider.hpk.base.enums
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
class ValueableSerializableEnumTest {
private val objectMapper = jacksonObjectMapper()
@Test
fun toValue_correctJacksonSerialization() {
val enum = TestEnum.VALUE1
val expected = enum.toValue()
val actual = objectMapper.writeValueAsString(enum)
.let(objectMapper::readTree)
.textValue()
Assertions.assertEquals(
expected,
actual
)
}
private enum class TestEnum(override val value: String) : ValueableSerializableEnum<String> {
VALUE1("testValue1"),
}
}

View File

@ -1,10 +1,10 @@
package ru.touchin.push.message.provider.hpk.clients.hms_hpk
import com.fasterxml.jackson.core.JsonParseException
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import ru.touchin.push.message.provider.hpk.clients.hms.enums.HmsResponseCode
import ru.touchin.push.message.provider.hpk.clients.hms_hpk.bodies.HmsHpkMessagesSendBody
import ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.AndroidConfig
import ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.Message
@ -22,39 +22,47 @@ class HmsHpkWebClientTest {
lateinit var hmsHpkWebClient: HmsHpkWebClient
@Test
fun messagesSend_pushTokenNotSpecified() {
val result = hmsHpkWebClient.messagesSend(
HmsHpkMessagesSendRequest(
hmsHpkMessagesSendBody = HmsHpkMessagesSendBody(
validateOnly = true,
message = Message.builder()
.addToken("pushTokenWithLongLength")
.setNotification(
Notification.builder()
.setTitle("title")
.setBody("body")
.setImage("https://avatars.githubusercontent.com/u/1435794?s=200&v=4")
.build()
)
.setAndroidConfig(
AndroidConfig.builder()
.setUrgency(AndroidUrgency.HIGH)
.setAndroidNotificationConfig(
AndroidNotificationConfig.builder()
.setDefaultSound(true)
.build(AndroidClickAction.builder().build(AndroidClickActionType.OPEN_APP))
)
.build()
)
.build()
),
accessToken = "testAccessToken"
fun messagesSend_returnsHtmlDocumentStringWith403CodeAtIncorrectAccessToken() {
val result = runCatching {
hmsHpkWebClient.messagesSend(
HmsHpkMessagesSendRequest(
hmsHpkMessagesSendBody = buildHmsHpkMessagesSendBody(
token = "pushTokenWithLongLength"
),
accessToken = "testAccessToken"
)
)
)
}
Assertions.assertEquals(
HmsResponseCode.PERMISSION_DENIED.value.toString(),
result.code
JsonParseException::class.java,
result.exceptionOrNull()?.cause?.let { it::class.java }
)
}
private fun buildHmsHpkMessagesSendBody(token: String): HmsHpkMessagesSendBody {
return HmsHpkMessagesSendBody(
validateOnly = true,
message = Message.builder()
.addToken(token)
.setNotification(
Notification.builder()
.setTitle("title")
.setBody("body")
.setImage("https://avatars.githubusercontent.com/u/1435794?s=200&v=4")
.build()
)
.setAndroidConfig(
AndroidConfig.builder()
.setUrgency(AndroidUrgency.HIGH)
.setAndroidNotificationConfig(
AndroidNotificationConfig.builder()
.setDefaultSound(true)
.build(AndroidClickAction.builder().build(AndroidClickActionType.OPEN_APP))
)
.build()
)
.build()
)
}

View File

@ -23,4 +23,5 @@ class HmsOauthWebClientTest {
HmsResponseCode.INVALID_CLIENT_SECRET.value
)
}
}

View File

@ -10,6 +10,7 @@ import org.springframework.boot.test.mock.mockito.MockBean
import ru.touchin.push.message.provider.dto.PushMessageNotification
import ru.touchin.push.message.provider.dto.request.PushTokenMessage
import ru.touchin.push.message.provider.exceptions.InvalidPushTokenException
import ru.touchin.push.message.provider.exceptions.PushMessageProviderException
import ru.touchin.push.message.provider.hpk.clients.hms_hpk.HmsHpkWebClient
import ru.touchin.push.message.provider.hpk.clients.hms.enums.HmsResponseCode
import ru.touchin.push.message.provider.hpk.clients.hms_hpk.responses.HmsHpkResponse
@ -27,8 +28,10 @@ class HmsHpkClientServiceTest {
lateinit var hmsHpkClientService: HmsHpkClientService
@Test
fun getAccessToken_throwsInvalidPushTokenExceptionForKnownErrors() {
Mockito.`when`(hmsOauthClientService.getAccessToken()).then { "accessToken" }
fun send_throwsInvalidPushTokenExceptionForKnownErrors() {
Mockito.`when`(
hmsOauthClientService.getAccessToken()
).then { "accessToken" }
Mockito.`when`(
hmsHpkWebClient.messagesSend(any())
@ -55,4 +58,66 @@ class HmsHpkClientServiceTest {
) { hmsHpkClientService.send(pushTokenMessage) }
}
@Test
fun send_throwsPushMessageProviderExceptionOnOtherExceptions() {
Mockito.`when`(
hmsOauthClientService.getAccessToken()
).then { "accessToken" }
Mockito.`when`(
hmsHpkWebClient.messagesSend(any())
).then {
HmsHpkResponse(
code = HmsResponseCode.OAUTH_TOKEN_EXPIRED.value.toString(),
msg = "0",
requestId = "requestId"
)
}
val pushTokenMessage = PushTokenMessage(
token = "token",
pushMessageNotification = PushMessageNotification(
title = "title",
description = "description",
imageUrl = null
),
data = emptyMap()
)
Assertions.assertThrows(
PushMessageProviderException::class.java
) { hmsHpkClientService.send(pushTokenMessage) }
}
@Test
fun send_passesSuccess() {
Mockito.`when`(
hmsOauthClientService.getAccessToken()
).then { "accessToken" }
Mockito.`when`(
hmsHpkWebClient.messagesSend(any())
).then {
HmsHpkResponse(
code = HmsResponseCode.SUCCESS.value.toString(),
msg = "0",
requestId = "requestId"
)
}
val pushTokenMessage = PushTokenMessage(
token = "token",
pushMessageNotification = PushMessageNotification(
title = "title",
description = "description",
imageUrl = null
),
data = emptyMap()
)
Assertions.assertDoesNotThrow {
hmsHpkClientService.send(pushTokenMessage)
}
}
}

View File

@ -74,6 +74,8 @@ class HmsOauthClientServiceTest {
@Test
fun getAccessToken_cachesExpectedTime() {
val expected = "accessToken"
Mockito.`when`(
hmsOauthWebClient.token()
).then {
@ -81,15 +83,18 @@ class HmsOauthClientServiceTest {
success = HmsOauthTokenResponse(
tokenType = "tokenType",
expiresIn = 60_000,
accessToken = "accessToken"
accessToken = expected
),
failure = null
)
}
Assertions.assertThrows(
InvalidPushTokenException::class.java
) { hmsOauthClientService.getAccessToken() }
val actual = hmsOauthClientService.getAccessToken()
Assertions.assertEquals(
expected,
actual
)
}

View File

@ -4,9 +4,9 @@ push-message-provider:
- HPK
hpk:
web-services:
client-id: 1
client-id: testClientId
oauth:
client-secret: 2
client-secret: testClientSecret
url: https://oauth-login.cloud.huawei.com/oauth2/v3/
http:
connection-timeout: 5s

View File

@ -0,0 +1,13 @@
plugins {
id("kotlin")
id("kotlin-spring")
}
dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
implementation("org.springframework.boot:spring-boot-autoconfigure")
implementation(project(":logger"))
implementation(project(":push-message-provider"))
}

View File

@ -0,0 +1,8 @@
@file:Suppress("unused")
package ru.touchin.push.message.provider.mock
import org.springframework.context.annotation.Import
import ru.touchin.push.message.provider.mock.configurations.PushMessageProviderMockConfiguration
@Import(value = [PushMessageProviderMockConfiguration::class])
annotation class EnablePushMessageProviderMock

View File

@ -0,0 +1,8 @@
package ru.touchin.push.message.provider.mock.configurations
import org.springframework.boot.context.properties.ConfigurationPropertiesScan
import org.springframework.context.annotation.ComponentScan
@ComponentScan("ru.touchin.push.message.provider.mock")
@ConfigurationPropertiesScan(basePackages = ["ru.touchin.push.message.provider.mock.properties"])
class PushMessageProviderMockConfiguration

View File

@ -0,0 +1,58 @@
package ru.touchin.push.message.provider.mock.factories
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.context.annotation.Primary
import org.springframework.stereotype.Service
import ru.touchin.logger.dto.LogData
import ru.touchin.logger.factory.LogBuilderFactory
import ru.touchin.push.message.provider.dto.request.PushTokenCheck
import ru.touchin.push.message.provider.dto.request.SendPushRequest
import ru.touchin.push.message.provider.dto.result.CheckPushTokenResult
import ru.touchin.push.message.provider.dto.result.SendPushResult
import ru.touchin.push.message.provider.dto.result.SendPushTokenMessageTraceableResult
import ru.touchin.push.message.provider.enums.PlatformType
import ru.touchin.push.message.provider.enums.PushMessageProviderType
import ru.touchin.push.message.provider.enums.PushTokenStatus
import ru.touchin.push.message.provider.factories.PushMessageProviderServiceFactory
import ru.touchin.push.message.provider.mock.properties.PushMessageProviderMockProperties
import ru.touchin.push.message.provider.services.PushMessageProviderService
import java.util.*
@Primary
@Service
@ConditionalOnProperty(prefix = "push-message-provider", name = ["mock.enabled"], havingValue = "true")
class PushMessageProviderMockServiceFactoryImpl(
private val logBuilderFactory: LogBuilderFactory<LogData>,
private val pushMessageProviderMockProperties: PushMessageProviderMockProperties,
) : PushMessageProviderServiceFactory {
override fun get(platformType: PlatformType): PushMessageProviderService {
return object : PushMessageProviderService {
override val type: PushMessageProviderType
get() = PushMessageProviderType.FCM
override fun send(request: SendPushRequest): SendPushResult {
val millis = pushMessageProviderMockProperties.sleepFor.toMillis()
logBuilderFactory.create(this::class.java)
.addTags("MOCK_PUSH_SEND")
.setMethod("send")
.addData("sleepForMs" to millis)
.build()
.log()
Thread.sleep(millis)
return SendPushTokenMessageTraceableResult(
messageId = UUID.randomUUID().toString(),
)
}
override fun check(request: PushTokenCheck): CheckPushTokenResult {
return CheckPushTokenResult(PushTokenStatus.VALID)
}
}
}
}

View File

@ -0,0 +1,11 @@
package ru.touchin.push.message.provider.mock.properties
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.ConstructorBinding
import java.time.Duration
@ConstructorBinding
@ConfigurationProperties(prefix = "push-message-provider.mock")
data class PushMessageProviderMockProperties(
val sleepFor: Duration,
)

View File

@ -4,6 +4,6 @@ import ru.touchin.push.message.provider.dto.PushMessageNotification
class PushTokenMessage(
val token: String,
val pushMessageNotification: PushMessageNotification?,
override val pushMessageNotification: PushMessageNotification?,
override val data: Map<String, String>
) : SendPushRequest

View File

@ -48,7 +48,7 @@ internal class ScopeRepositoryTest {
val savedScope = scopeRepository.findByIdOrThrow(scope.name)
assertTrue(
ReflectionEquals(scope, "createdAt").matches(savedScope)
ReflectionEquals(scope, "createdAt", "users").matches(savedScope)
)
}

View File

@ -51,7 +51,7 @@ internal class UserRepositoryTest {
val savedUser = userRepository.findByIdOrThrow(user.id!!)
assertTrue(ReflectionEquals(user, "createdAt").matches(savedUser))
assertTrue(ReflectionEquals(user, "createdAt", "confirmedAt").matches(savedUser))
}

View File

@ -1,6 +1,5 @@
package ru.touchin.auth.security.jwks.controllers
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

View File

@ -1,7 +1,5 @@
package ru.touchin.server.info.services
import org.springframework.util.MultiValueMap
interface ServerInfoHeader {
fun getHeaders(): Map<String, String>

View File

@ -1,10 +1,8 @@
package ru.touchin.server.info.services.version
import org.springframework.stereotype.Component
import org.springframework.util.LinkedMultiValueMap
import org.springframework.util.MultiValueMap
import ru.touchin.server.info.services.version.properties.ServerInfoProperties
import ru.touchin.server.info.services.ServerInfoHeader
import ru.touchin.server.info.services.version.properties.ServerInfoProperties
@Component
class BuildVersionHeader(

View File

@ -3,11 +3,15 @@ pluginManagement {
val kotlinVersion: String by settings
val springBootVersion: String by settings
val springDependencyManagementVersion: String by settings
val detektGradlePluginVersion: String by settings
val diktatGradlePluginVersion: String by settings
val versions = listOf(
"org.jetbrains.kotlin" to kotlinVersion,
"org.springframework.boot" to springBootVersion,
"io.spring.dependency-management" to springDependencyManagementVersion
"io.spring.dependency-management" to springDependencyManagementVersion,
"io.gitlab.arturbosch.detekt" to detektGradlePluginVersion,
"org.cqfn.diktat.diktat-gradle-plugin" to diktatGradlePluginVersion,
)
eachPlugin {
@ -35,6 +39,7 @@ include("common-geo")
include("common-geo-spatial4j-spring")
include("common-messaging")
include("captcha")
include("codestyle-archunit")
include("logger")
include("logger-spring")
include("logger-spring-web")
@ -46,6 +51,7 @@ include("version-spring-web")
include("push-message-provider")
include("push-message-provider-fcm")
include("push-message-provider-hpk")
include("push-message-provider-mock")
include("response-wrapper-spring-web")
include("settings-spring-jpa")
include("security-authorization-server-core")
@ -59,3 +65,5 @@ include("s3-storage")
include("server-info-spring-web")
include("geoip-core")
include("user-agent")
include("smart-migration")
include("telegram-bot-spring")

View File

@ -0,0 +1,12 @@
plugins {
id("kotlin")
id("kotlin-spring")
id("maven-publish")
}
dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
implementation("org.springframework.boot:spring-boot")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation project(":common-spring")
}

View File

@ -0,0 +1,59 @@
@file:Suppress("unused")
package ru.touchin.smartmigration
import org.springframework.stereotype.Component
import ru.touchin.common.spring.annotations.RequiredBy
import ru.touchin.smartmigration.logic.DataSourceSQL
import ru.touchin.smartmigration.logic.factory.DataSourceSqlFactoryImpl
import java.sql.Date
import java.text.SimpleDateFormat
import javax.annotation.PostConstruct
import javax.sql.DataSource
private val SQL_DATE_FORMAT = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS")
private val CURRENT_TIME_SQL: String
get() = SQL_DATE_FORMAT.format(Date(System.currentTimeMillis()))
@Component
@RequiredBy("liquibase")
class BeforeLiquibase(
private val dataSource: DataSource
) {
val dataSourceSql: DataSourceSQL = DataSourceSqlFactoryImpl()
.getDataSourceSql(dataSource.connection.metaData.databaseProductName)
@PostConstruct
fun doAction() {
val buildNumber = System.getenv("BUILD_NUMBER")
?: return
checkMigrationTable()
if (checkBuildMigrationExecuted(buildNumber)) {
System.setProperty("spring.liquibase.enabled", "false")
} else {
insertMigration(buildNumber)
}
}
private fun checkBuildMigrationExecuted(buildNumber: String): Boolean {
return dataSourceSql.getMigrationCheckSQL(buildNumber).let {
dataSource.connection.createStatement().executeQuery(it).next()
}
}
private fun checkMigrationTable() {
dataSourceSql.getTableCheckSQL().let {
dataSource.connection.createStatement().execute(it)
}
}
private fun insertMigration(buildNumber: String) {
dataSourceSql.getInsertMigrationSQL(buildNumber, CURRENT_TIME_SQL).let {
dataSource.connection.createStatement().execute(it)
}
}
}

View File

@ -0,0 +1,8 @@
package ru.touchin.smartmigration
import org.springframework.context.annotation.ComponentScan
import org.springframework.context.annotation.Configuration
@Configuration
@ComponentScan("ru.touchin.smartmigration")
open class SmartMigrationConfig

View File

@ -0,0 +1,7 @@
package ru.touchin.smartmigration.annotation
import org.springframework.context.annotation.Import
import ru.touchin.smartmigration.SmartMigrationConfig
@Import(SmartMigrationConfig::class)
annotation class EnableSmartMigration

View File

@ -0,0 +1,11 @@
package ru.touchin.smartmigration.logic
interface DataSourceSQL {
fun getTableCheckSQL(): String
fun getMigrationCheckSQL(buildNumber: String): String
fun getInsertMigrationSQL(buildNumber: String, formattedTime: String): String
}

View File

@ -0,0 +1,33 @@
package ru.touchin.smartmigration.logic
import org.intellij.lang.annotations.Language
private const val MIGRATION_TABLE_NAME = "SMART_MIGRATION"
class PostgresDataSourceImpl: DataSourceSQL {
@Language("SQL")
override fun getTableCheckSQL(): String {
return """
CREATE TABLE IF NOT EXISTS smart_migration (
ID BIGSERIAL PRIMARY KEY,
BUILD_NUMBER VARCHAR(255) NOT NULL,
DATE timestamp NOT NULL
);
"""
}
@Language("SQL")
override fun getMigrationCheckSQL(buildNumber: String): String {
return "SELECT * FROM $MIGRATION_TABLE_NAME WHERE BUILD_NUMBER = '$buildNumber'"
}
@Language("SQL")
override fun getInsertMigrationSQL(buildNumber: String, formattedTime: String): String {
return """
INSERT INTO $MIGRATION_TABLE_NAME (BUILD_NUMBER, DATE)
VALUES ('$buildNumber', '$formattedTime');
"""
}
}

View File

@ -0,0 +1,38 @@
package ru.touchin.smartmigration.logic
import org.intellij.lang.annotations.Language
private const val MIGRATION_TABLE_NAME = "SMART_MIGRATION"
class SqlDatasourceImpl:DataSourceSQL {
@Language("TSQL")
override fun getTableCheckSQL(): String {
return """
IF NOT EXISTS (
SELECT * FROM sysobjects WHERE name = '$MIGRATION_TABLE_NAME' and xtype='U'
)
CREATE TABLE SMART_MIGRATION (
ID BIGINT PRIMARY KEY IDENTITY ,
BUILD_NUMBER VARCHAR(255) NOT NULL,
DATE DATETIME NOT NULL
);
"""
}
@Language("TSQL")
override fun getMigrationCheckSQL(buildNumber: String): String {
return "SELECT * FROM $MIGRATION_TABLE_NAME WHERE BUILD_NUMBER = '$buildNumber'"
}
@Language("TSQL")
override fun getInsertMigrationSQL(buildNumber: String, formattedTime: String): String {
return """
INSERT INTO $MIGRATION_TABLE_NAME (BUILD_NUMBER, DATE)
VALUES ('$buildNumber', '$formattedTime');
"""
}
}

View File

@ -0,0 +1,9 @@
package ru.touchin.smartmigration.logic.factory
import ru.touchin.smartmigration.logic.DataSourceSQL
interface DataSourceSqlFactory {
fun getDataSourceSql(driverName: String): DataSourceSQL
}

View File

@ -0,0 +1,19 @@
package ru.touchin.smartmigration.logic.factory
import ru.touchin.smartmigration.logic.DataSourceSQL
import ru.touchin.smartmigration.logic.PostgresDataSourceImpl
import ru.touchin.smartmigration.logic.SqlDatasourceImpl
class DataSourceSqlFactoryImpl: DataSourceSqlFactory {
override fun getDataSourceSql(driverName: String): DataSourceSQL {
return when(driverName){
"Microsoft SQL Server" -> SqlDatasourceImpl()
"PostgresSQL" -> PostgresDataSourceImpl()
else -> {
PostgresDataSourceImpl()
}
}
}
}

37
telegram-bot-spring/.gitignore vendored Normal file
View File

@ -0,0 +1,37 @@
HELP.md
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
out/
!**/src/main/**/out/
!**/src/test/**/out/
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
### VS Code ###
.vscode/

View File

@ -0,0 +1,35 @@
## Библиотека написания телеграмм-ботов
```kotlin
@Component
class MyTelegramBot(
private val messageHandlers: List<MessageHandler>,
) : TelegramLongPollingBot() {
override fun onUpdateReceived(update: Update?) {
// create MessageContext
// messageHandlers.takeWhile { it.process(ctx, this) }
}
}
@Component
class HelloMessageHandler: AbstractMessageHandler {
override fun isSupported(ctx: MessageContext): Booleat {
ctx.messageCommand.message.equals("hi")
}
override fun process(ctx: MessageContext, sender: AbsSender): Boolean {
val message = SendMessage().apply {
this.chatId = ctx.chatId
this.text = "Hello"
}
sender.execute(message)
return true
}
}
```

View File

@ -0,0 +1,18 @@
plugins {
id 'kotlin'
id 'kotlin-spring'
}
dependencies {
api 'org.telegram:telegrambots-spring-boot-starter:6.3.0'
implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8'
implementation 'com.fasterxml.jackson.core:jackson-databind'
implementation 'com.fasterxml.jackson.module:jackson-module-kotlin'
implementation 'org.springframework.boot:spring-boot'
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin")
testImplementation("org.mockito:mockito-inline")
testImplementation 'junit:junit'
}

View File

@ -0,0 +1,6 @@
package ru.touchin.spring.telegram.bot
import org.springframework.context.annotation.Import
@Import(value = [TelegramBotConfiguration::class])
annotation class EnableSpringTelegramBot

View File

@ -0,0 +1,12 @@
package ru.touchin.spring.telegram.bot
import org.springframework.boot.context.properties.ConfigurationPropertiesScan
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.ComponentScan
import org.springframework.context.annotation.Configuration
@Configuration
@EnableConfigurationProperties
@ConfigurationPropertiesScan
@ComponentScan
class TelegramBotConfiguration

View File

@ -0,0 +1,22 @@
package ru.touchin.spring.telegram.bot.messages
import org.telegram.telegrambots.meta.bots.AbsSender
import ru.touchin.spring.telegram.bot.messages.dto.MessageContext
abstract class AbstractMessageHandler<C: MessageContext<U, S>, U, S>:
MessageHandler<C, U, S>,
MessageFilter<C, U, S>,
HelpMessage
{
override fun process(context: C, sender: AbsSender): Boolean {
if (!isSupported(context)) {
return false
}
return internalProcess(context, sender)
}
abstract fun internalProcess(context: C, sender: AbsSender): Boolean
}

View File

@ -0,0 +1,7 @@
package ru.touchin.spring.telegram.bot.messages
interface HelpMessage {
fun helpMessage(): String? = null
}

View File

@ -0,0 +1,9 @@
package ru.touchin.spring.telegram.bot.messages
import ru.touchin.spring.telegram.bot.messages.dto.MessageContext
interface MessageFilter<C: MessageContext<U, S>, U, S> {
fun isSupported(context: C): Boolean
}

View File

@ -0,0 +1,10 @@
package ru.touchin.spring.telegram.bot.messages
import org.telegram.telegrambots.meta.bots.AbsSender
import ru.touchin.spring.telegram.bot.messages.dto.MessageContext
interface MessageHandler<C: MessageContext<U, S>, U, S> {
fun process(context: C, sender: AbsSender): Boolean
}

View File

@ -0,0 +1,8 @@
package ru.touchin.spring.telegram.bot.messages
@Suppress("unused")
object Order {
const val HIGH_PRIORITY = 1024
const val NORMAL_PRIORITY = 2048
const val LOW_PRIORITY = 3072
}

View File

@ -0,0 +1,19 @@
package ru.touchin.spring.telegram.bot.messages.dto
data class MessageCommand(
val command: String,
val message: String
) {
fun hasCommand() = command != NO_COMMAND
companion object {
private const val NO_COMMAND = ""
fun createNoCommandMessage(message: String) = MessageCommand(
command = NO_COMMAND,
message = message
)
}
}

View File

@ -0,0 +1,17 @@
package ru.touchin.spring.telegram.bot.messages.dto
import org.telegram.telegrambots.meta.api.objects.Update
open class MessageContext<U, S>(
val origin: Update,
val user: U,
val messageCommand: MessageCommand,
val state: S
) {
fun hasCommand(command: String) = messageCommand.command == command
val chatId: String
get() = origin.message?.chatId?.toString()
?: origin.callbackQuery.message.chatId.toString()
}

Some files were not shown because too many files have changed in this diff Show More