Compare commits
21 Commits
feature/pu
...
master
| Author | SHA1 | Date |
|---|---|---|
|
|
5e0fce2a60 | |
|
|
a070a94381 | |
|
|
0976db1e47 | |
|
|
e17454b996 | |
|
|
c8ed908656 | |
|
|
20f07a4a9d | |
|
|
a85e655aba | |
|
|
3c837cf1d1 | |
|
|
0fda706e9f | |
|
|
9281a5d213 | |
|
|
844c0a9c73 | |
|
|
d2ddf042e6 | |
|
|
8da83cea86 | |
|
|
8e08f8a570 | |
|
|
7710442654 | |
|
|
171cacf30d | |
|
|
723e5a4178 | |
|
|
faf1e92633 | |
|
|
eef1504171 | |
|
|
1d63ab6156 | |
|
|
709c71e48e |
219
README.md
219
README.md
|
|
@ -19,6 +19,19 @@
|
||||||
implementation("ru.touchin:common")
|
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
|
## 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
|
## logger
|
||||||
|
|
||||||
Основные компоненты логирования:
|
Основные компоненты логирования:
|
||||||
|
|
@ -216,7 +329,47 @@ server.info:
|
||||||
|
|
||||||
## push-message-provider
|
## 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
|
## push-message-provider-fcm
|
||||||
|
|
||||||
|
|
@ -231,7 +384,7 @@ push-message-provider:
|
||||||
IOS:
|
IOS:
|
||||||
- FCM
|
- FCM
|
||||||
fcm:
|
fcm:
|
||||||
appName: # Название приложения
|
appName: yourAppName
|
||||||
auth:
|
auth:
|
||||||
# Выбранный тип авторизации
|
# Выбранный тип авторизации
|
||||||
client:
|
client:
|
||||||
|
|
@ -258,16 +411,60 @@ C) Данные из файла консоли Firebase, добавляемые
|
||||||
auth:
|
auth:
|
||||||
credentialsData:
|
credentialsData:
|
||||||
type: service_account
|
type: service_account
|
||||||
projectId: testProjectId
|
projectId: yourProjectId
|
||||||
privateKeyId: testPrivateKeyId
|
privateKeyId: yourPrivateKeyId
|
||||||
privateKey: |
|
privateKey: |
|
||||||
-----BEGIN PRIVATE KEY-----
|
-----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-----
|
-----END PRIVATE KEY-----
|
||||||
clientEmail: testClientEmail
|
clientEmail: yourClientEmail
|
||||||
clientId: testClientId
|
clientId: yourClientId
|
||||||
authUri: testAuthUri
|
authUri: yourAuthUri
|
||||||
tokenUri: testTokenUri
|
tokenUri: yourTokenUri
|
||||||
authProviderX509CertUrl: testAuthProviderX509CertUrl
|
authProviderX509CertUrl: yourAuthProviderX509CertUrl
|
||||||
clientX509CertUrl: testClientX509CertUrl
|
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
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||||
import io.spring.gradle.dependencymanagement.dsl.DependencyManagementExtension
|
import io.spring.gradle.dependencymanagement.dsl.DependencyManagementExtension
|
||||||
|
import io.gitlab.arturbosch.detekt.extensions.DetektExtension
|
||||||
|
import org.jetbrains.kotlin.cli.common.toBooleanLenient
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
kotlin("jvm")
|
kotlin("jvm")
|
||||||
id ("org.springframework.boot") apply false
|
id("org.springframework.boot") apply false
|
||||||
|
|
||||||
// IntelliJ
|
// IntelliJ
|
||||||
idea
|
idea
|
||||||
|
|
@ -15,6 +17,9 @@ plugins {
|
||||||
// A Gradle plugin that provides Maven-like dependency management and exclusions
|
// A Gradle plugin that provides Maven-like dependency management and exclusions
|
||||||
// https://docs.spring.io/dependency-management-plugin/docs/current/reference/html/
|
// https://docs.spring.io/dependency-management-plugin/docs/current/reference/html/
|
||||||
id("io.spring.dependency-management")
|
id("io.spring.dependency-management")
|
||||||
|
|
||||||
|
id("io.gitlab.arturbosch.detekt")
|
||||||
|
id("org.cqfn.diktat.diktat-gradle-plugin")
|
||||||
}
|
}
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
|
|
@ -30,6 +35,30 @@ allprojects {
|
||||||
apply(plugin = "idea")
|
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 {
|
subprojects {
|
||||||
println("Enabling Kotlin JVM plugin in project ${project.name}...")
|
println("Enabling Kotlin JVM plugin in project ${project.name}...")
|
||||||
apply(plugin = "org.jetbrains.kotlin.jvm")
|
apply(plugin = "org.jetbrains.kotlin.jvm")
|
||||||
|
|
@ -40,6 +69,26 @@ subprojects {
|
||||||
println("Enabling Spring Boot Dependency Management in project ${project.name}...")
|
println("Enabling Spring Boot Dependency Management in project ${project.name}...")
|
||||||
apply(plugin = "io.spring.dependency-management")
|
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> {
|
configure<DependencyManagementExtension> {
|
||||||
imports {
|
imports {
|
||||||
mavenBom(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)
|
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-json-classic:0.1.5")
|
||||||
dependency("ch.qos.logback.contrib:logback-jackson:0.1.5")
|
dependency("ch.qos.logback.contrib:logback-jackson:0.1.5")
|
||||||
|
|
||||||
dependency("org.testcontainers:testcontainers:1.15.1")
|
dependency("org.testcontainers:testcontainers:1.18.3")
|
||||||
dependency("org.testcontainers:postgresql:1.15.1")
|
dependency("org.testcontainers:postgresql:1.18.3")
|
||||||
dependency("org.testcontainers:junit-jupiter:1.15.1")
|
dependency("org.testcontainers:junit-jupiter:1.18.3")
|
||||||
dependency("org.junit.jupiter:junit-jupiter-api:5.4.2")
|
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-params:5.4.2")
|
||||||
dependency("org.junit.jupiter:junit-jupiter-engine: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("org.liquibase:liquibase-core:4.4.0")
|
||||||
|
|
||||||
dependency("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0")
|
dependency("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0")
|
||||||
|
|
@ -74,13 +125,15 @@ subprojects {
|
||||||
dependency("software.amazon.awssdk:s3:2.10.11")
|
dependency("software.amazon.awssdk:s3:2.10.11")
|
||||||
|
|
||||||
dependency("com.google.firebase:firebase-admin:9.0.0")
|
dependency("com.google.firebase:firebase-admin:9.0.0")
|
||||||
|
|
||||||
|
dependency("com.github.ua-parser:uap-java:1.5.3")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// use for @ConstructorBinding
|
// use for @ConstructorBinding
|
||||||
implementation("org.jetbrains.kotlin:kotlin-reflect")
|
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")
|
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
plugins {
|
||||||
|
id("kotlin")
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(project(":common"))
|
||||||
|
implementation("com.tngtech.archunit:archunit")
|
||||||
|
}
|
||||||
|
|
@ -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")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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'")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
plugins {
|
||||||
|
id("kotlin")
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package ru.touchin.auth.core.device.dto.enums
|
package ru.touchin.common.devices.enums
|
||||||
|
|
||||||
enum class DevicePlatform {
|
enum class DevicePlatform {
|
||||||
Android, Huawei, Apple, Web
|
Android, Huawei, Apple, Web
|
||||||
|
|
@ -13,7 +13,7 @@ import javax.persistence.MappedSuperclass
|
||||||
@MappedSuperclass
|
@MappedSuperclass
|
||||||
abstract class BaseEntity : Serializable {
|
abstract class BaseEntity : Serializable {
|
||||||
|
|
||||||
@CreatedDate
|
@CreatedDate(updatable = false)
|
||||||
lateinit var createdAt: ZonedDateTime
|
lateinit var createdAt: ZonedDateTime
|
||||||
|
|
||||||
@LastModifiedDate
|
@LastModifiedDate
|
||||||
|
|
|
||||||
|
|
@ -15,5 +15,9 @@ abstract class BaseUuidIdEntity : BaseEntity() {
|
||||||
@GeneratedValue(generator = "uuid")
|
@GeneratedValue(generator = "uuid")
|
||||||
@GenericGenerator(name = "uuid", strategy = "uuid2")
|
@GenericGenerator(name = "uuid", strategy = "uuid2")
|
||||||
open var id: UUID? = null
|
open var id: UUID? = null
|
||||||
|
@Suppress("RedundantSetter")
|
||||||
|
protected set(id) {
|
||||||
|
field = id
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -19,12 +19,12 @@ object StringUtils {
|
||||||
}
|
}
|
||||||
|
|
||||||
nextUpperCase -> {
|
nextUpperCase -> {
|
||||||
this.append(char.toUpperCase())
|
this.append(char.uppercase())
|
||||||
nextUpperCase = false
|
nextUpperCase = false
|
||||||
}
|
}
|
||||||
|
|
||||||
!nextUpperCase -> {
|
else -> {
|
||||||
this.append(char.toLowerCase())
|
this.append(char.lowercase())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -34,8 +34,8 @@ object StringUtils {
|
||||||
fun String.removeNonPrintableCharacters(): String {
|
fun String.removeNonPrintableCharacters(): String {
|
||||||
return this
|
return this
|
||||||
.transliterateCyrillic()
|
.transliterateCyrillic()
|
||||||
.replace("[\\p{Cntrl}&&[^\r\n\t]]".toRegex(), "")// erases all the ASCII control characters
|
.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{C}".toRegex(), "") // removes non-printable characters from Unicode
|
||||||
.trim()
|
.trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.*'
|
||||||
|
|
@ -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
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
package ru.touchin.exception.handler.spring.configurations
|
package ru.touchin.exception.handler.spring.configurations
|
||||||
|
|
||||||
import org.springframework.context.annotation.Bean
|
import org.springframework.context.annotation.Bean
|
||||||
import org.springframework.context.annotation.ComponentScan
|
import org.springframework.context.annotation.ComponentScan
|
||||||
import org.springframework.context.annotation.Configuration
|
import org.springframework.context.annotation.Configuration
|
||||||
|
|
|
||||||
|
|
@ -33,10 +33,14 @@ class ExceptionHandlerAdvice(
|
||||||
|
|
||||||
val body = exceptionResponseBodyCreator(result.apiError)
|
val body = exceptionResponseBodyCreator(result.apiError)
|
||||||
|
|
||||||
val headers = if (exceptionResolverProperties.includeHeaders) HttpHeaders().apply {
|
val headers = if (exceptionResolverProperties.includeHeaders) {
|
||||||
set("X-Error-Code", result.apiError.errorCode.toString())
|
HttpHeaders().apply {
|
||||||
set("X-Error-Message", result.apiError.errorMessage)
|
set("X-Error-Code", result.apiError.errorCode.toString())
|
||||||
} else null
|
set("X-Error-Message", result.apiError.errorMessage)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
return ResponseEntity(body, headers, result.status)
|
return ResponseEntity(body, headers, result.status)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
kotlin.code.style=official
|
kotlin.code.style=official
|
||||||
springBootVersion=2.5.3
|
springBootVersion=2.5.3
|
||||||
springDependencyManagementVersion=1.0.11.RELEASE
|
springDependencyManagementVersion=1.0.11.RELEASE
|
||||||
kotlinVersion=1.5.21
|
detektGradlePluginVersion=1.18.0
|
||||||
|
diktatGradlePluginVersion=1.2.5
|
||||||
|
kotlinVersion=1.6.21
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
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
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import org.aspectj.lang.ProceedingJoinPoint
|
||||||
import org.aspectj.lang.annotation.Around
|
import org.aspectj.lang.annotation.Around
|
||||||
import org.aspectj.lang.annotation.Aspect
|
import org.aspectj.lang.annotation.Aspect
|
||||||
import org.aspectj.lang.reflect.MethodSignature
|
import org.aspectj.lang.reflect.MethodSignature
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
import ru.touchin.logger.spring.annotations.AutoLogging
|
import ru.touchin.logger.spring.annotations.AutoLogging
|
||||||
import ru.touchin.logger.spring.annotations.LogValue
|
import ru.touchin.logger.spring.annotations.LogValue
|
||||||
import ru.touchin.logger.builder.LogDataItem
|
import ru.touchin.logger.builder.LogDataItem
|
||||||
|
|
@ -22,6 +23,7 @@ class LogAspect(
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@Around("@annotation(autoLoggingAnnotation)")
|
@Around("@annotation(autoLoggingAnnotation)")
|
||||||
|
@Suppress("TooGenericExceptionCaught")
|
||||||
fun logInvocation(pjp: ProceedingJoinPoint, autoLoggingAnnotation: AutoLogging): Any? {
|
fun logInvocation(pjp: ProceedingJoinPoint, autoLoggingAnnotation: AutoLogging): Any? {
|
||||||
val duration = LogDuration()
|
val duration = LogDuration()
|
||||||
|
|
||||||
|
|
@ -46,8 +48,11 @@ class LogAspect(
|
||||||
.build()
|
.build()
|
||||||
.error()
|
.error()
|
||||||
} catch (logError: Throwable) {
|
} catch (logError: Throwable) {
|
||||||
error.printStackTrace()
|
LoggerFactory.getLogger(this::class.java)
|
||||||
logError.printStackTrace()
|
.let { logger ->
|
||||||
|
logger.error("Cannot build logger", error)
|
||||||
|
logger.error("Cannot create logger", logError)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ interface LogBuilder<T> {
|
||||||
fun addData(vararg items: LogDataItem): LogBuilder<T>
|
fun addData(vararg items: LogDataItem): LogBuilder<T>
|
||||||
fun setMethod(method: String): LogBuilder<T>
|
fun setMethod(method: String): LogBuilder<T>
|
||||||
fun setContext(context: LogExecutionContextData): LogBuilder<LogData>
|
fun setContext(context: LogExecutionContextData): LogBuilder<LogData>
|
||||||
|
fun setContext(): LogBuilder<LogData>
|
||||||
fun build(): Log<T>
|
fun build(): Log<T>
|
||||||
fun isEnabled(logLevel: LogLevel): Boolean
|
fun isEnabled(logLevel: LogLevel): Boolean
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,10 +38,14 @@ class LogBuilderImpl(
|
||||||
logData.method = method
|
logData.method = method
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setContext(context: LogExecutionContextData): LogBuilder<LogData> = also {
|
override fun setContext(): LogBuilder<LogData> = also {
|
||||||
logData.ctx = LoggerExecutionContext.current.get()
|
logData.ctx = LoggerExecutionContext.current.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun setContext(context: LogExecutionContextData): LogBuilder<LogData> = also {
|
||||||
|
logData.ctx = context
|
||||||
|
}
|
||||||
|
|
||||||
override fun build(): Log<LogData> {
|
override fun build(): Log<LogData> {
|
||||||
if (logData.ctx == null) {
|
if (logData.ctx == null) {
|
||||||
logData.ctx = LoggerExecutionContext.current.get()
|
logData.ctx = LoggerExecutionContext.current.get()
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
package ru.touchin.logger.context
|
package ru.touchin.logger.context
|
||||||
|
|
||||||
@Suppress("unused", "EnumEntryName")
|
@Suppress("unused", "EnumEntryName", "EnumNaming")
|
||||||
enum class DefaultContextFields {
|
enum class DefaultContextFields {
|
||||||
id,
|
id,
|
||||||
host,
|
host,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
package ru.touchin.logger.dto
|
package ru.touchin.logger.dto
|
||||||
|
|
||||||
enum class LogLevel {
|
enum class LogLevel {
|
||||||
Trace, Info, Error
|
Trace, Debug, Info, Error
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
package ru.touchin.logger.log
|
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 org.slf4j.LoggerFactory
|
||||||
import ru.touchin.logger.dto.LogLevel
|
import ru.touchin.logger.dto.LogLevel
|
||||||
import ru.touchin.logger.dto.LogData
|
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() {
|
override fun info() {
|
||||||
if (logger.isInfoEnabled) {
|
if (logger.isInfoEnabled) {
|
||||||
val logMessage = getMessage()
|
val logMessage = getMessage()
|
||||||
|
|
@ -53,9 +66,20 @@ abstract class AbstractLog(clazz: Class<*>) : Log<LogData> {
|
||||||
override fun isEnabled(level: LogLevel): Boolean {
|
override fun isEnabled(level: LogLevel): Boolean {
|
||||||
return when(level) {
|
return when(level) {
|
||||||
LogLevel.Trace -> logger.isTraceEnabled
|
LogLevel.Trace -> logger.isTraceEnabled
|
||||||
|
LogLevel.Debug -> logger.isDebugEnabled
|
||||||
LogLevel.Info -> logger.isInfoEnabled
|
LogLevel.Info -> logger.isInfoEnabled
|
||||||
LogLevel.Error -> logger.isErrorEnabled
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,20 @@
|
||||||
package ru.touchin.logger.log
|
package ru.touchin.logger.log
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonInclude
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode
|
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.databind.node.ObjectNode
|
||||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
|
|
||||||
import ru.touchin.logger.dto.LogError
|
import ru.touchin.logger.dto.LogError
|
||||||
import ru.touchin.logger.dto.LogError.Companion.stackTraceAsString
|
import ru.touchin.logger.dto.LogError.Companion.stackTraceAsString
|
||||||
import ru.touchin.logger.dto.LogError.Companion.toLogError
|
import ru.touchin.logger.dto.LogError.Companion.toLogError
|
||||||
|
|
||||||
open class JsonLogImpl(clazz: Class<*>) : AbstractLog(clazz) {
|
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 {
|
private fun toJson(): JsonNode {
|
||||||
val result = objectMapper.convertValue(logData, ObjectNode::class.java)
|
val result = objectMapper().convertValue(logData, ObjectNode::class.java)
|
||||||
|
|
||||||
logData.error
|
logData.error
|
||||||
?.toLogError()
|
?.toLogError()
|
||||||
?.let {
|
?.let {
|
||||||
objectMapper.convertValue(it, JsonNode::class.java)
|
objectMapper().convertValue(it, JsonNode::class.java)
|
||||||
}
|
}
|
||||||
?.let {
|
?.let {
|
||||||
result.remove("error")
|
result.remove("error")
|
||||||
|
|
@ -39,7 +26,7 @@ open class JsonLogImpl(clazz: Class<*>) : AbstractLog(clazz) {
|
||||||
|
|
||||||
override fun getMessage(): LogMessage {
|
override fun getMessage(): LogMessage {
|
||||||
val message = runCatching {
|
val message = runCatching {
|
||||||
objectMapper.writeValueAsString(toJson())
|
objectMapper().writeValueAsString(toJson())
|
||||||
}.getOrElse { throwable ->
|
}.getOrElse { throwable ->
|
||||||
"""
|
"""
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@ import ru.touchin.logger.dto.LogLevel
|
||||||
interface Log<T> {
|
interface Log<T> {
|
||||||
|
|
||||||
var logData: T
|
var logData: T
|
||||||
|
|
||||||
fun trace()
|
fun trace()
|
||||||
|
fun debug()
|
||||||
fun info()
|
fun info()
|
||||||
fun error()
|
fun error()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ class SimpleLogImpl(clazz: Class<*>): AbstractLog(clazz) {
|
||||||
|
|
||||||
override fun getMessage(): LogMessage {
|
override fun getMessage(): LogMessage {
|
||||||
val builder = StringBuilder()
|
val builder = StringBuilder()
|
||||||
|
val pretty = objectMapper().writerWithDefaultPrettyPrinter()
|
||||||
|
|
||||||
builder.append("\n\ttags: ${logData.tags.joinToString(",")}")
|
builder.append("\n\ttags: ${logData.tags.joinToString(",")}")
|
||||||
|
|
||||||
|
|
@ -11,6 +12,18 @@ class SimpleLogImpl(clazz: Class<*>): AbstractLog(clazz) {
|
||||||
builder.append("\n\tduration: ${logData.duration}ms")
|
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(
|
return LogMessage(
|
||||||
message = builder.toString(),
|
message = builder.toString(),
|
||||||
error = logData.error
|
error = logData.error
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import org.springframework.stereotype.Component
|
||||||
import ru.touchin.push.message.provider.dto.request.PushTokenCheck
|
import ru.touchin.push.message.provider.dto.request.PushTokenCheck
|
||||||
import ru.touchin.push.message.provider.dto.request.PushTokenMessage
|
import ru.touchin.push.message.provider.dto.request.PushTokenMessage
|
||||||
import ru.touchin.push.message.provider.dto.result.SendPushResult
|
import ru.touchin.push.message.provider.dto.result.SendPushResult
|
||||||
import ru.touchin.push.message.provider.dto.result.SendPushTokenMessageResult
|
import ru.touchin.push.message.provider.dto.result.SendPushTokenMessageTraceableResult
|
||||||
import ru.touchin.push.message.provider.enums.PushTokenStatus
|
import ru.touchin.push.message.provider.enums.PushTokenStatus
|
||||||
import ru.touchin.push.message.provider.exceptions.InvalidPushTokenException
|
import ru.touchin.push.message.provider.exceptions.InvalidPushTokenException
|
||||||
import ru.touchin.push.message.provider.exceptions.PushMessageProviderException
|
import ru.touchin.push.message.provider.exceptions.PushMessageProviderException
|
||||||
|
|
@ -32,7 +32,7 @@ class FcmClient(
|
||||||
fun check(request: PushTokenCheck): PushTokenStatus {
|
fun check(request: PushTokenCheck): PushTokenStatus {
|
||||||
val validationRequest = PushTokenMessage(
|
val validationRequest = PushTokenMessage(
|
||||||
token = request.pushToken,
|
token = request.pushToken,
|
||||||
notification = null,
|
pushMessageNotification = null,
|
||||||
data = emptyMap()
|
data = emptyMap()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -54,7 +54,7 @@ class FcmClient(
|
||||||
return try {
|
return try {
|
||||||
val messageId = firebaseMessaging.send(message, dryRun)
|
val messageId = firebaseMessaging.send(message, dryRun)
|
||||||
|
|
||||||
SendPushTokenMessageResult(messageId)
|
SendPushTokenMessageTraceableResult(messageId)
|
||||||
} catch (e: FirebaseMessagingException) {
|
} catch (e: FirebaseMessagingException) {
|
||||||
throw firebaseMessagingExceptionConverter(e)
|
throw firebaseMessagingExceptionConverter(e)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
package ru.touchin.push.message.provider.fcm.configurations
|
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.ObjectMapper
|
||||||
|
import com.fasterxml.jackson.databind.SerializationFeature
|
||||||
import com.google.auth.oauth2.AccessToken
|
import com.google.auth.oauth2.AccessToken
|
||||||
import com.google.auth.oauth2.GoogleCredentials
|
import com.google.auth.oauth2.GoogleCredentials
|
||||||
import com.google.firebase.FirebaseApp
|
import com.google.firebase.FirebaseApp
|
||||||
|
|
@ -25,6 +28,7 @@ class PushMessageProviderFcmConfiguration {
|
||||||
@Bean
|
@Bean
|
||||||
fun firebaseMessaging(
|
fun firebaseMessaging(
|
||||||
properties: PushMessageProviderFcmProperties,
|
properties: PushMessageProviderFcmProperties,
|
||||||
|
@Qualifier("push-message-provider.fcm.credentials-object-mapper")
|
||||||
objectMapper: ObjectMapper
|
objectMapper: ObjectMapper
|
||||||
): FirebaseMessaging {
|
): FirebaseMessaging {
|
||||||
val credentials = when {
|
val credentials = when {
|
||||||
|
|
@ -60,10 +64,22 @@ class PushMessageProviderFcmConfiguration {
|
||||||
return FirebaseMessaging.getInstance(firebaseApp)
|
return FirebaseMessaging.getInstance(firebaseApp)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean("push-message-provider.fcm.credentials-date-format")
|
||||||
@Qualifier("push-message-provider.fcm.auth")
|
|
||||||
fun simpleDateFormat(): SimpleDateFormat {
|
fun simpleDateFormat(): SimpleDateFormat {
|
||||||
return SimpleDateFormat("yyyy-MM-dd HH:mm:ss X", Locale.getDefault())
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.beans.factory.annotation.Qualifier
|
||||||
import org.springframework.boot.context.properties.ConfigurationPropertiesBinding
|
import org.springframework.boot.context.properties.ConfigurationPropertiesBinding
|
||||||
|
|
@ -10,12 +10,12 @@ import java.util.*
|
||||||
@ConfigurationPropertiesBinding
|
@ConfigurationPropertiesBinding
|
||||||
@Component
|
@Component
|
||||||
class DateConverter(
|
class DateConverter(
|
||||||
@Qualifier("push-message-provider.fcm.auth")
|
@Qualifier("push-message-provider.fcm.credentials-date-format")
|
||||||
private val simpleDateFormat: SimpleDateFormat
|
private val simpleDateFormat: SimpleDateFormat
|
||||||
) : Converter<String, Date> {
|
) : Converter<String, Date> {
|
||||||
|
|
||||||
override fun convert(source: String?): Date? {
|
override fun convert(source: String): Date {
|
||||||
return source?.let(simpleDateFormat::parse)
|
return simpleDateFormat.parse(source)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -13,11 +13,17 @@ class FirebaseMessagingExceptionConverter {
|
||||||
operator fun invoke(exception: FirebaseMessagingException): CommonException {
|
operator fun invoke(exception: FirebaseMessagingException): CommonException {
|
||||||
return when (exception.messagingErrorCode) {
|
return when (exception.messagingErrorCode) {
|
||||||
MessagingErrorCode.INVALID_ARGUMENT,
|
MessagingErrorCode.INVALID_ARGUMENT,
|
||||||
MessagingErrorCode.UNREGISTERED -> InvalidPushTokenException()
|
MessagingErrorCode.UNREGISTERED,
|
||||||
else -> PushMessageProviderException(
|
MessagingErrorCode.SENDER_ID_MISMATCH -> {
|
||||||
description = exception.message.orEmpty(),
|
InvalidPushTokenException()
|
||||||
cause = exception
|
}
|
||||||
)
|
|
||||||
|
else -> {
|
||||||
|
PushMessageProviderException(
|
||||||
|
description = exception.message.orEmpty(),
|
||||||
|
cause = exception
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
package ru.touchin.push.message.provider.fcm.converters
|
|
||||||
|
|
||||||
import com.google.firebase.messaging.Notification as FcmNotification
|
|
||||||
import org.springframework.stereotype.Component
|
|
||||||
import ru.touchin.push.message.provider.dto.Notification
|
|
||||||
|
|
||||||
@Component
|
|
||||||
class NotificationConverter {
|
|
||||||
|
|
||||||
operator fun invoke(notification: Notification): FcmNotification {
|
|
||||||
return FcmNotification.builder()
|
|
||||||
.setTitle(notification.title)
|
|
||||||
.setBody(notification.description)
|
|
||||||
.setImage(notification.imageUrl)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
package ru.touchin.push.message.provider.fcm.converters
|
||||||
|
|
||||||
|
import com.google.firebase.messaging.Notification as FcmNotification
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import ru.touchin.push.message.provider.dto.PushMessageNotification
|
||||||
|
|
||||||
|
@Component("push-message-provider.fcm.push-message-notification-converter")
|
||||||
|
class PushMessageNotificationConverter {
|
||||||
|
|
||||||
|
operator fun invoke(pushMessageNotification: PushMessageNotification): FcmNotification {
|
||||||
|
return FcmNotification.builder()
|
||||||
|
.setTitle(pushMessageNotification.title)
|
||||||
|
.setBody(pushMessageNotification.description)
|
||||||
|
.setImage(pushMessageNotification.imageUrl)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,29 +1,67 @@
|
||||||
package ru.touchin.push.message.provider.fcm.converters
|
package ru.touchin.push.message.provider.fcm.converters
|
||||||
|
|
||||||
|
import com.google.firebase.messaging.AndroidConfig
|
||||||
|
import com.google.firebase.messaging.AndroidNotification
|
||||||
|
import com.google.firebase.messaging.ApnsConfig
|
||||||
|
import com.google.firebase.messaging.Aps
|
||||||
import com.google.firebase.messaging.Message
|
import com.google.firebase.messaging.Message
|
||||||
import org.springframework.stereotype.Component
|
import org.springframework.stereotype.Component
|
||||||
import ru.touchin.push.message.provider.dto.Notification
|
import ru.touchin.push.message.provider.dto.PushMessageNotification
|
||||||
import ru.touchin.push.message.provider.dto.request.PushTokenMessage
|
import ru.touchin.push.message.provider.dto.request.PushTokenMessage
|
||||||
|
|
||||||
@Component
|
@Component("push-message-provider.fcm.push-token-message-converter")
|
||||||
class PushTokenMessageConverter(
|
class PushTokenMessageConverter(
|
||||||
private val notificationConverter: NotificationConverter
|
private val pushMessageNotificationConverter: PushMessageNotificationConverter
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
|
||||||
|
const val PLATFORMS_KEY_SOUND_ON = "default"
|
||||||
|
const val IOS_ENABLE_BACKGROUND_UPDATE = true
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
operator fun invoke(request: PushTokenMessage): Message {
|
operator fun invoke(request: PushTokenMessage): Message {
|
||||||
return Message.builder()
|
return Message.builder()
|
||||||
.setToken(request.token)
|
.setToken(request.token)
|
||||||
.setIfExists(request.notification)
|
.setupApns()
|
||||||
|
.setupAndroid()
|
||||||
|
.setIfExists(request.pushMessageNotification)
|
||||||
.putAllData(request.data)
|
.putAllData(request.data)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Message.Builder.setIfExists(notification: Notification?): Message.Builder {
|
private fun Message.Builder.setIfExists(pushMessageNotification: PushMessageNotification?): Message.Builder {
|
||||||
return if (notification != null) {
|
return if (pushMessageNotification != null) {
|
||||||
setNotification(notificationConverter(notification))
|
setNotification(pushMessageNotificationConverter(pushMessageNotification))
|
||||||
} else {
|
} else {
|
||||||
this
|
this
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun Message.Builder.setupApns(): Message.Builder {
|
||||||
|
return setApnsConfig(
|
||||||
|
ApnsConfig.builder()
|
||||||
|
.setAps(
|
||||||
|
Aps.builder()
|
||||||
|
.setSound(PLATFORMS_KEY_SOUND_ON)
|
||||||
|
.setContentAvailable(IOS_ENABLE_BACKGROUND_UPDATE)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Message.Builder.setupAndroid(): Message.Builder {
|
||||||
|
return setAndroidConfig(
|
||||||
|
AndroidConfig.builder()
|
||||||
|
.setNotification(
|
||||||
|
AndroidNotification.builder()
|
||||||
|
.setSound(PLATFORMS_KEY_SOUND_ON)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,22 @@
|
||||||
package ru.touchin.push.message.provider.fcm
|
package ru.touchin.push.message.provider.fcm
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonAutoDetect
|
import com.google.firebase.FirebaseApp
|
||||||
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 org.springframework.boot.SpringBootConfiguration
|
import org.springframework.boot.SpringBootConfiguration
|
||||||
import org.springframework.boot.test.context.TestConfiguration
|
import org.springframework.boot.test.context.TestConfiguration
|
||||||
import org.springframework.context.annotation.Bean
|
import org.springframework.context.ApplicationListener
|
||||||
import java.text.SimpleDateFormat
|
import org.springframework.context.event.ContextRefreshedEvent
|
||||||
|
|
||||||
@TestConfiguration
|
@TestConfiguration
|
||||||
@SpringBootConfiguration
|
@SpringBootConfiguration
|
||||||
@EnablePushMessageProviderFcm
|
@EnablePushMessageProviderFcm
|
||||||
class PushMessageProviderFcmTestApplication {
|
class PushMessageProviderFcmTestApplication : ApplicationListener<ContextRefreshedEvent> {
|
||||||
|
|
||||||
@Bean
|
override fun onApplicationEvent(event: ContextRefreshedEvent) {
|
||||||
fun objectMapper(
|
clearSingletonsOutsideContainer()
|
||||||
@Qualifier("push-message-provider.fcm.auth")
|
}
|
||||||
simpleDateFormat: SimpleDateFormat
|
|
||||||
): ObjectMapper {
|
private fun clearSingletonsOutsideContainer() {
|
||||||
return ObjectMapper().apply {
|
FirebaseApp.getApps().forEach(FirebaseApp::delete)
|
||||||
configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false)
|
|
||||||
setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE)
|
|
||||||
setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY)
|
|
||||||
dateFormat = simpleDateFormat
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,15 +5,15 @@ import org.junit.Assert
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
import org.springframework.boot.test.context.SpringBootTest
|
import org.springframework.boot.test.context.SpringBootTest
|
||||||
import ru.touchin.push.message.provider.dto.Notification
|
import ru.touchin.push.message.provider.dto.PushMessageNotification
|
||||||
import com.google.firebase.messaging.Notification as FcmNotification
|
import com.google.firebase.messaging.Notification as FcmNotification
|
||||||
import org.junit.jupiter.api.DisplayName
|
import org.junit.jupiter.api.DisplayName
|
||||||
|
|
||||||
@SpringBootTest
|
@SpringBootTest
|
||||||
class NotificationConverterTest {
|
class PushMessageNotificationConverterTest {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
lateinit var notificationConverter: NotificationConverter
|
lateinit var pushMessageNotificationConverter: PushMessageNotificationConverter
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
lateinit var objectMapper: ObjectMapper
|
lateinit var objectMapper: ObjectMapper
|
||||||
|
|
@ -21,19 +21,19 @@ class NotificationConverterTest {
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("Конвертация уведомления происходит корректно")
|
@DisplayName("Конвертация уведомления происходит корректно")
|
||||||
fun invoke_basic() {
|
fun invoke_basic() {
|
||||||
val notification = Notification(
|
val pushMessageNotification = PushMessageNotification(
|
||||||
title = "title",
|
title = "title",
|
||||||
description = "description",
|
description = "description",
|
||||||
imageUrl = "imageUrl"
|
imageUrl = "imageUrl"
|
||||||
)
|
)
|
||||||
|
|
||||||
val realResult = notificationConverter(notification)
|
val realResult = pushMessageNotificationConverter(pushMessageNotification)
|
||||||
val realResultJson = objectMapper.writeValueAsString(realResult)
|
val realResultJson = objectMapper.writeValueAsString(realResult)
|
||||||
|
|
||||||
val expectedResult = FcmNotification.builder()
|
val expectedResult = FcmNotification.builder()
|
||||||
.setTitle(notification.title)
|
.setTitle(pushMessageNotification.title)
|
||||||
.setBody(notification.description)
|
.setBody(pushMessageNotification.description)
|
||||||
.setImage(notification.imageUrl)
|
.setImage(pushMessageNotification.imageUrl)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val expectedResultJson = objectMapper.writeValueAsString(expectedResult)
|
val expectedResultJson = objectMapper.writeValueAsString(expectedResult)
|
||||||
|
|
@ -1,13 +1,17 @@
|
||||||
package ru.touchin.push.message.provider.fcm.converters
|
package ru.touchin.push.message.provider.fcm.converters
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import com.google.firebase.messaging.AndroidConfig
|
||||||
|
import com.google.firebase.messaging.AndroidNotification
|
||||||
|
import com.google.firebase.messaging.ApnsConfig
|
||||||
|
import com.google.firebase.messaging.Aps
|
||||||
import com.google.firebase.messaging.Message
|
import com.google.firebase.messaging.Message
|
||||||
import org.junit.Assert
|
import org.junit.Assert
|
||||||
import org.junit.jupiter.api.DisplayName
|
import org.junit.jupiter.api.DisplayName
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
import org.springframework.boot.test.context.SpringBootTest
|
import org.springframework.boot.test.context.SpringBootTest
|
||||||
import ru.touchin.push.message.provider.dto.Notification
|
import ru.touchin.push.message.provider.dto.PushMessageNotification
|
||||||
import ru.touchin.push.message.provider.dto.request.PushTokenMessage
|
import ru.touchin.push.message.provider.dto.request.PushTokenMessage
|
||||||
|
|
||||||
@SpringBootTest
|
@SpringBootTest
|
||||||
|
|
@ -17,7 +21,7 @@ class PushTokenMessageConverterTest {
|
||||||
lateinit var pushTokenMessageConverter: PushTokenMessageConverter
|
lateinit var pushTokenMessageConverter: PushTokenMessageConverter
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
lateinit var notificationConverter: NotificationConverter
|
lateinit var pushMessageNotificationConverter: PushMessageNotificationConverter
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
lateinit var objectMapper: ObjectMapper
|
lateinit var objectMapper: ObjectMapper
|
||||||
|
|
@ -25,14 +29,14 @@ class PushTokenMessageConverterTest {
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("Конвертация сообщения с уведомлением происходит корректно")
|
@DisplayName("Конвертация сообщения с уведомлением происходит корректно")
|
||||||
fun invoke_withNotification() {
|
fun invoke_withNotification() {
|
||||||
val notification = Notification(
|
val pushMessageNotification = PushMessageNotification(
|
||||||
title = "title",
|
title = "title",
|
||||||
description = "description",
|
description = "description",
|
||||||
imageUrl = "imageUrl"
|
imageUrl = "imageUrl"
|
||||||
)
|
)
|
||||||
val pushTokenMessage = PushTokenMessage(
|
val pushTokenMessage = PushTokenMessage(
|
||||||
token = "token",
|
token = "token",
|
||||||
notification = notification,
|
pushMessageNotification = pushMessageNotification,
|
||||||
data = mapOf("testKey" to "testvalue")
|
data = mapOf("testKey" to "testvalue")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -41,8 +45,10 @@ class PushTokenMessageConverterTest {
|
||||||
|
|
||||||
val expectedResult = Message.builder()
|
val expectedResult = Message.builder()
|
||||||
.setToken(pushTokenMessage.token)
|
.setToken(pushTokenMessage.token)
|
||||||
.setNotification(notificationConverter(notification))
|
.setNotification(pushMessageNotificationConverter(pushMessageNotification))
|
||||||
.putAllData(pushTokenMessage.data)
|
.putAllData(pushTokenMessage.data)
|
||||||
|
.setupApns()
|
||||||
|
.setupAndroid()
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val expectedResultJson = objectMapper.writeValueAsString(expectedResult)
|
val expectedResultJson = objectMapper.writeValueAsString(expectedResult)
|
||||||
|
|
@ -59,7 +65,7 @@ class PushTokenMessageConverterTest {
|
||||||
fun invoke_withoutNotification() {
|
fun invoke_withoutNotification() {
|
||||||
val pushTokenMessage = PushTokenMessage(
|
val pushTokenMessage = PushTokenMessage(
|
||||||
token = "token",
|
token = "token",
|
||||||
notification = null,
|
pushMessageNotification = null,
|
||||||
data = mapOf("testKey" to "testvalue")
|
data = mapOf("testKey" to "testvalue")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -69,6 +75,8 @@ class PushTokenMessageConverterTest {
|
||||||
val expectedResult = Message.builder()
|
val expectedResult = Message.builder()
|
||||||
.setToken(pushTokenMessage.token)
|
.setToken(pushTokenMessage.token)
|
||||||
.putAllData(pushTokenMessage.data)
|
.putAllData(pushTokenMessage.data)
|
||||||
|
.setupApns()
|
||||||
|
.setupAndroid()
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val expectedResultJson = objectMapper.writeValueAsString(expectedResult)
|
val expectedResultJson = objectMapper.writeValueAsString(expectedResult)
|
||||||
|
|
@ -80,4 +88,29 @@ class PushTokenMessageConverterTest {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun Message.Builder.setupApns(): Message.Builder {
|
||||||
|
return setApnsConfig(
|
||||||
|
ApnsConfig.builder()
|
||||||
|
.setAps(
|
||||||
|
Aps.builder()
|
||||||
|
.setSound("default")
|
||||||
|
.setContentAvailable(true)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Message.Builder.setupAndroid(): Message.Builder {
|
||||||
|
return setAndroidConfig(
|
||||||
|
AndroidConfig.builder()
|
||||||
|
.setNotification(
|
||||||
|
AndroidNotification.builder()
|
||||||
|
.setSound("default")
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import org.springframework.boot.test.mock.mockito.MockBean
|
||||||
import ru.touchin.push.message.provider.dto.request.PushTokenCheck
|
import ru.touchin.push.message.provider.dto.request.PushTokenCheck
|
||||||
import ru.touchin.push.message.provider.dto.request.PushTokenMessage
|
import ru.touchin.push.message.provider.dto.request.PushTokenMessage
|
||||||
import ru.touchin.push.message.provider.dto.result.CheckPushTokenResult
|
import ru.touchin.push.message.provider.dto.result.CheckPushTokenResult
|
||||||
import ru.touchin.push.message.provider.dto.result.SendPushTokenMessageResult
|
import ru.touchin.push.message.provider.dto.result.SendPushTokenMessageTraceableResult
|
||||||
import ru.touchin.push.message.provider.enums.PushTokenStatus
|
import ru.touchin.push.message.provider.enums.PushTokenStatus
|
||||||
import ru.touchin.push.message.provider.fcm.clients.FcmClient
|
import ru.touchin.push.message.provider.fcm.clients.FcmClient
|
||||||
import ru.touchin.push.message.provider.services.PushMessageProviderService
|
import ru.touchin.push.message.provider.services.PushMessageProviderService
|
||||||
|
|
@ -29,11 +29,11 @@ class PushMessageProviderFcmServiceTest {
|
||||||
fun send_basic() {
|
fun send_basic() {
|
||||||
val request = PushTokenMessage(
|
val request = PushTokenMessage(
|
||||||
token = "testToken",
|
token = "testToken",
|
||||||
notification = null,
|
pushMessageNotification = null,
|
||||||
data = emptyMap()
|
data = emptyMap()
|
||||||
)
|
)
|
||||||
|
|
||||||
val expectedResult = SendPushTokenMessageResult("testMessageId")
|
val expectedResult = SendPushTokenMessageTraceableResult("testMessageId")
|
||||||
|
|
||||||
Mockito.`when`(
|
Mockito.`when`(
|
||||||
fcmClient.sendPushTokenMessage(request)
|
fcmClient.sendPushTokenMessage(request)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
plugins {
|
||||||
|
id("kotlin")
|
||||||
|
id("kotlin-spring")
|
||||||
|
id("maven-publish")
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
|
||||||
|
|
||||||
|
implementation("org.springframework.boot:spring-boot-starter-webflux")
|
||||||
|
|
||||||
|
implementation(project(":logger-spring"))
|
||||||
|
implementation(project(":common-spring-web"))
|
||||||
|
implementation(project(":push-message-provider"))
|
||||||
|
|
||||||
|
testImplementation(project(":logger-spring-web"))
|
||||||
|
|
||||||
|
testImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin")
|
||||||
|
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
||||||
|
testImplementation("org.testcontainers:junit-jupiter")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package ru.touchin.push.message.provider.hpk
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Import
|
||||||
|
import ru.touchin.push.message.provider.hpk.configurations.PushMessageProviderHpkConfiguration
|
||||||
|
|
||||||
|
@Import(value = [PushMessageProviderHpkConfiguration::class])
|
||||||
|
annotation class EnablePushMessageProviderHpk
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
package ru.touchin.push.message.provider.hpk.base.builders
|
||||||
|
|
||||||
|
internal interface Buildable
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
package ru.touchin.push.message.provider.hpk.base.clients
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import org.springframework.web.reactive.function.client.ClientResponse
|
||||||
|
import ru.touchin.push.message.provider.hpk.base.clients.dto.ConditionalResponse
|
||||||
|
|
||||||
|
internal open class ConditionalWebClientParser(
|
||||||
|
private val objectMapper: ObjectMapper,
|
||||||
|
) {
|
||||||
|
|
||||||
|
open fun isOkResponse(clientResponse: ClientResponse): Boolean {
|
||||||
|
return clientResponse.statusCode().is2xxSuccessful
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
inline fun <reified S, reified F> parse(
|
||||||
|
clientResponse: ClientResponse,
|
||||||
|
body: String,
|
||||||
|
): ConditionalResponse<S, F> {
|
||||||
|
return if (isOkResponse(clientResponse)) {
|
||||||
|
ConditionalResponse<S, F>(
|
||||||
|
success = parseValue(body, S::class.java),
|
||||||
|
failure = null
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ConditionalResponse(
|
||||||
|
success = null,
|
||||||
|
failure = parseValue(body, F::class.java)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <T> parseValue(source: String?, clazz: Class<T>): T {
|
||||||
|
return if (clazz.canonicalName != String::class.java.canonicalName) {
|
||||||
|
objectMapper.readValue(source, clazz)
|
||||||
|
} else {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
source as T // T is String
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
package ru.touchin.push.message.provider.hpk.base.clients
|
||||||
|
|
||||||
|
import io.netty.channel.ChannelOption
|
||||||
|
import io.netty.handler.ssl.SslContextBuilder
|
||||||
|
import io.netty.handler.timeout.ReadTimeoutHandler
|
||||||
|
import io.netty.handler.timeout.WriteTimeoutHandler
|
||||||
|
import org.springframework.http.client.reactive.ReactorClientHttpConnector
|
||||||
|
import org.springframework.web.reactive.function.client.ClientResponse
|
||||||
|
import org.springframework.web.reactive.function.client.WebClient
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
import reactor.core.scheduler.Schedulers
|
||||||
|
import reactor.netty.http.client.HttpClient
|
||||||
|
import ru.touchin.common.spring.web.webclient.BaseLogWebClient
|
||||||
|
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.dto.ConditionalResponse
|
||||||
|
import ru.touchin.push.message.provider.hpk.properties.HpkProperties
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
abstract class ConfigurableWebClient(
|
||||||
|
webClientLogger: WebClientLogger,
|
||||||
|
webClientBuilder: WebClient.Builder,
|
||||||
|
protected val webService: HpkProperties.WebService,
|
||||||
|
) : BaseLogWebClient(webClientLogger, webClientBuilder) {
|
||||||
|
|
||||||
|
private val conditionalWebClientParser: Lazy<ConditionalWebClientParser> = lazy {
|
||||||
|
ConditionalWebClientParser(
|
||||||
|
objectMapper = getObjectMapper(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun WebClient.Builder.setTimeouts(): WebClient.Builder {
|
||||||
|
val httpClient: HttpClient = HttpClient.create()
|
||||||
|
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, webService.http.connectionTimeout.toMillis().toInt())
|
||||||
|
.doOnConnected { setup ->
|
||||||
|
setup.addHandlerLast(ReadTimeoutHandler(webService.http.readTimeout.toMillis(), TimeUnit.MILLISECONDS))
|
||||||
|
setup.addHandlerLast(WriteTimeoutHandler(webService.http.writeTimeout.toMillis(), TimeUnit.MILLISECONDS))
|
||||||
|
}
|
||||||
|
.let { httpClient ->
|
||||||
|
webService.ssl?.let { ssl ->
|
||||||
|
httpClient.secure { builder ->
|
||||||
|
builder
|
||||||
|
.sslContext(SslContextBuilder.forClient().build())
|
||||||
|
.handshakeTimeout(ssl.handshakeTimeout)
|
||||||
|
.closeNotifyFlushTimeout(ssl.notifyFlushTimeout)
|
||||||
|
.closeNotifyReadTimeout(ssl.notifyReadTimeout)
|
||||||
|
}
|
||||||
|
} ?: httpClient
|
||||||
|
}
|
||||||
|
|
||||||
|
return clientConnector(ReactorClientHttpConnector(httpClient))
|
||||||
|
}
|
||||||
|
|
||||||
|
internal inline fun <reified S, reified F> WebClient.RequestHeadersSpec<*>.exchangeWithWrap(
|
||||||
|
requestLogData: RequestLogData,
|
||||||
|
): Mono<ConditionalResponse<S, F>> {
|
||||||
|
return exchangeToMono { clientResponse ->
|
||||||
|
parse<S, F>(clientResponse)
|
||||||
|
}.doOnNext { responseWrapper ->
|
||||||
|
getLogger().log(
|
||||||
|
requestLogData.copy(
|
||||||
|
responseBody = responseWrapper.success ?: responseWrapper.failure
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal inline fun <reified S, reified F> parse(
|
||||||
|
clientResponse: ClientResponse,
|
||||||
|
): Mono<ConditionalResponse<S, F>> {
|
||||||
|
val responseBody = clientResponse
|
||||||
|
.bodyToMono(String::class.java)
|
||||||
|
.defaultIfEmpty(String())
|
||||||
|
.publishOn(Schedulers.parallel())
|
||||||
|
|
||||||
|
return responseBody
|
||||||
|
.map { body ->
|
||||||
|
conditionalWebClientParser.value.parse(
|
||||||
|
clientResponse = clientResponse,
|
||||||
|
body = body,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
package ru.touchin.push.message.provider.hpk.base.clients.dto
|
||||||
|
|
||||||
|
internal open class ConditionalResponse<S, F>(
|
||||||
|
val success: S?,
|
||||||
|
val failure: F?,
|
||||||
|
) {
|
||||||
|
|
||||||
|
init {
|
||||||
|
// Only one value should be present
|
||||||
|
val hasSuccessValue = success != null
|
||||||
|
val hasFailureValue = failure != null
|
||||||
|
|
||||||
|
check(hasSuccessValue != hasFailureValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
val isSuccess: Boolean = success != null
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
package ru.touchin.push.message.provider.hpk.base.enums
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonValue
|
||||||
|
|
||||||
|
internal interface ValueableSerializableEnum<T> {
|
||||||
|
|
||||||
|
val value: T
|
||||||
|
|
||||||
|
@JsonValue
|
||||||
|
fun toValue(): T = value
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package ru.touchin.push.message.provider.hpk.base.extensions
|
||||||
|
|
||||||
|
import ru.touchin.push.message.provider.hpk.base.builders.Buildable
|
||||||
|
|
||||||
|
fun <V, B : Buildable> B.ifNotNull(value: V?, setter: B.(V) -> B): B {
|
||||||
|
return value?.let { setter(it) } ?: this
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
package ru.touchin.push.message.provider.hpk.clients.hms.enums
|
||||||
|
|
||||||
|
import ru.touchin.push.message.provider.hpk.base.enums.ValueableSerializableEnum
|
||||||
|
|
||||||
|
internal enum class HmsResponseCode(
|
||||||
|
override val value: Int,
|
||||||
|
val description: String,
|
||||||
|
) : ValueableSerializableEnum<Int> {
|
||||||
|
|
||||||
|
UNKNOWN(-1, "Unknown"),
|
||||||
|
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"),
|
||||||
|
PUSH_TOKEN_NOT_SPECIFIED(80100002, "Token number should be one when send sys message"),
|
||||||
|
INCORRECT_MESSAGE_STRUCTURE(80100003, "Incorrect message structure"),
|
||||||
|
TTL_IS_INVALID(80100004, "TTL is less than current time, please check"),
|
||||||
|
COLLAPSE_KEY_IS_INVALID(80100013, "Collapse_key is illegal, please check"),
|
||||||
|
MESSAGE_DATA_IS_VULNERABLE(80100016, "Message contains sensitive information, please check"),
|
||||||
|
TOPIC_AMOUNT_EXCEEDED(80100017, "A maximum of 100 topic-based messages can be sent at the same time"),
|
||||||
|
INVALID_MESSAGE_BODY(80100018, "Invalid message body"),
|
||||||
|
OAUTH_AUTHENTICATION_ERROR(80200001, "Oauth authentication error"),
|
||||||
|
OAUTH_TOKEN_EXPIRED(80200003, "Oauth Token expired"),
|
||||||
|
PERMISSION_DENIED(80300002, "There is no permission to send a message to a specified device"),
|
||||||
|
INVALID_TOKEN(80300007, "The specified token is invalid"),
|
||||||
|
MESSAGE_SIZE_EXCEEDED(80300008, "The message body size exceeds the default value set by the system (4K)"),
|
||||||
|
TOKEN_AMOUNT_EXCEEDED(80300010, "Tokens exceed the default value"),
|
||||||
|
MESSAGE_PERMISSION_DENIED(80300011, "No permission to send high-level notification messages"),
|
||||||
|
OAUTH_SERVER_ERROR(80600003, "Request OAuth service failed"),
|
||||||
|
INTERNAL_SERVER_ERROR(81000001, "System inner error"),
|
||||||
|
GROUP_ERROR(82000001, "GroupKey or groupName error"),
|
||||||
|
GROUP_MISMATCH(82000002, "GroupKey and groupName do not match"),
|
||||||
|
INVALID_TOKEN_ARRAY(82000003, "Token array is null"),
|
||||||
|
GROUP_NOT_EXIST(82000004, "Group do not exist"),
|
||||||
|
GROUP_APP_MISMATCH(82000005, "Group do not belong to this app"),
|
||||||
|
INVALID_TOKEN_ARRAY_OR_GROUP(82000006, "Token array or group number is transfinited"),
|
||||||
|
INVALID_TOPIC(82000007, "Invalid topic"),
|
||||||
|
TOKEN_AMOUNT_IS_NULL_OR_EXCEEDED(82000008, "Token array null or transfinited"),
|
||||||
|
TOO_MANY_TOPICS(82000009, "Topic amount exceeded: at most 2000"),
|
||||||
|
SOME_TOKENS_ARE_INCORRECT(82000010, "Some tokens are incorrect"),
|
||||||
|
TOKEN_IS_NULL(82000011, "Token is null"),
|
||||||
|
DATA_LOCATION_NOT_SPECIFIED(82000012, "Data storage location is not selected"),
|
||||||
|
DATA_LOCATION_MISMATCH(82000013, "Data storage location does not match the actual data");
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
fun fromCode(code: String): HmsResponseCode {
|
||||||
|
return values().find { it.value.toString() == code }
|
||||||
|
?: UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
package ru.touchin.push.message.provider.hpk.clients.hms_hpk
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier
|
||||||
|
import org.springframework.http.HttpMethod
|
||||||
|
import org.springframework.http.MediaType
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import org.springframework.web.reactive.function.BodyInserters
|
||||||
|
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.requests.HmsHpkMessagesSendRequest
|
||||||
|
import ru.touchin.push.message.provider.hpk.clients.hms_hpk.responses.HmsHpkResponse
|
||||||
|
import ru.touchin.push.message.provider.hpk.properties.HpkProperties
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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")
|
||||||
|
private val objectMapper: ObjectMapper,
|
||||||
|
) : ConfigurableWebClient(webClientLogger, webClientBuilder, hpkProperties.webServices.hpk) {
|
||||||
|
|
||||||
|
override fun getObjectMapper(): ObjectMapper = objectMapper
|
||||||
|
|
||||||
|
override fun getWebClient(): WebClient {
|
||||||
|
return getWebClientBuilder(
|
||||||
|
url = webService.url.toString(),
|
||||||
|
)
|
||||||
|
.setTimeouts()
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun messagesSend(hmsHpkMessagesSendRequest: HmsHpkMessagesSendRequest): HmsHpkResponse {
|
||||||
|
val url = "${hpkProperties.webServices.clientId}/$METHOD_MESSAGES_SEND"
|
||||||
|
|
||||||
|
return getWebClient().post()
|
||||||
|
.uri { builder ->
|
||||||
|
builder
|
||||||
|
.path(url)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.headers { it.setBearerAuth(hmsHpkMessagesSendRequest.accessToken) }
|
||||||
|
.body(BodyInserters.fromValue(hmsHpkMessagesSendRequest.hmsHpkMessagesSendBody))
|
||||||
|
.exchange(
|
||||||
|
clazz = HmsHpkResponse::class.java,
|
||||||
|
requestLogData = RequestLogData(
|
||||||
|
uri = url,
|
||||||
|
logTags = listOf(),
|
||||||
|
method = HttpMethod.POST,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.block() ?: throw IllegalStateException("No response")
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
|
||||||
|
const val METHOD_MESSAGES_SEND = "messages:send"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
package ru.touchin.push.message.provider.hpk.clients.hms_hpk.bodies
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.Message
|
||||||
|
|
||||||
|
internal class HmsHpkMessagesSendBody(
|
||||||
|
/** Send "dry" message without notification delivery */
|
||||||
|
val validateOnly: Boolean,
|
||||||
|
/** Message structure, which must contain the valid message payload and valid sending object. */
|
||||||
|
@JsonProperty("message")
|
||||||
|
val message: Message,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,165 @@
|
||||||
|
package ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto
|
||||||
|
|
||||||
|
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.dto.android.AndroidNotificationConfig
|
||||||
|
import ru.touchin.push.message.provider.hpk.clients.hms_hpk.enums.android.AndroidFastAppTargetType
|
||||||
|
import ru.touchin.push.message.provider.hpk.clients.hms_hpk.enums.android.AndroidUrgency
|
||||||
|
|
||||||
|
internal data class AndroidConfig private constructor(
|
||||||
|
/**
|
||||||
|
* Mode for the Push Kit server to cache messages sent to an offline device.
|
||||||
|
* These cached messages will be delivered once the device goes online again.
|
||||||
|
* The options are as follows:
|
||||||
|
*
|
||||||
|
* 0: Only the latest message sent by each app to the user device is cached.
|
||||||
|
*
|
||||||
|
* -1: All messages are cached.
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* The default value is -1.
|
||||||
|
* */
|
||||||
|
val collapseKey: Short?,
|
||||||
|
@JsonProperty("urgency")
|
||||||
|
val androidUrgency: AndroidUrgency?,
|
||||||
|
val category: String?,
|
||||||
|
/**
|
||||||
|
* Message cache duration, in seconds.
|
||||||
|
* When a user device is offline, the Push Kit server caches messages.
|
||||||
|
* If the user device goes online within the message cache time, the cached messages are delivered.
|
||||||
|
* Otherwise, the messages are discarded.
|
||||||
|
* */
|
||||||
|
val ttl: String?,
|
||||||
|
/**
|
||||||
|
* Tag of a message in a batch delivery task.
|
||||||
|
* The tag is returned to your server when Push Kit sends the message receipt.
|
||||||
|
* */
|
||||||
|
val biTag: String?,
|
||||||
|
/** State of a mini program when a quick app sends a data message. Default is [AndroidFastAppTargetType.PRODUCTION]*/
|
||||||
|
@JsonProperty("fast_app_target")
|
||||||
|
val androidFastAppTargetType: AndroidFastAppTargetType?,
|
||||||
|
/** Custom message payload. If the data parameter is set, the value of the [Message.data] field is overwritten. */
|
||||||
|
val data: String?,
|
||||||
|
@JsonProperty("notification")
|
||||||
|
val androidNotificationConfig: AndroidNotificationConfig?,
|
||||||
|
/** Unique receipt ID that associates with the receipt URL and configuration of the downlink message. */
|
||||||
|
val receiptId: String?,
|
||||||
|
) {
|
||||||
|
|
||||||
|
class Validator {
|
||||||
|
|
||||||
|
fun check(androidConfig: AndroidConfig, notification: Notification?) {
|
||||||
|
with(androidConfig) {
|
||||||
|
if (collapseKey != null) {
|
||||||
|
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" }
|
||||||
|
}
|
||||||
|
if (androidNotificationConfig != null) {
|
||||||
|
AndroidNotificationConfig.validator.check(androidNotificationConfig, notification)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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]")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class Builder : Buildable {
|
||||||
|
|
||||||
|
private var collapseKey: Short? = null
|
||||||
|
private var androidUrgency: AndroidUrgency? = null
|
||||||
|
private var category: String? = null
|
||||||
|
private var ttl: String? = null
|
||||||
|
private var biTag: String? = null
|
||||||
|
private var androidFastAppTargetType: AndroidFastAppTargetType? = null
|
||||||
|
private var data: String? = null
|
||||||
|
private var androidNotificationConfig: AndroidNotificationConfig? = null
|
||||||
|
private var receiptId: String? = null
|
||||||
|
|
||||||
|
fun setCollapseKey(collapseKey: Short): Builder {
|
||||||
|
this.collapseKey = collapseKey
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setUrgency(androidUrgency: AndroidUrgency): Builder {
|
||||||
|
this.androidUrgency = androidUrgency
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setCategory(category: String?): Builder {
|
||||||
|
this.category = category
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setTtl(ttl: String): Builder {
|
||||||
|
this.ttl = ttl
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setBiTag(biTag: String): Builder {
|
||||||
|
this.biTag = biTag
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setFastAppTargetType(androidFastAppTargetType: AndroidFastAppTargetType): Builder {
|
||||||
|
this.androidFastAppTargetType = androidFastAppTargetType
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setData(data: String): Builder {
|
||||||
|
this.data = data
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setAndroidNotificationConfig(androidNotificationConfig: AndroidNotificationConfig): Builder {
|
||||||
|
this.androidNotificationConfig = androidNotificationConfig
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setReceiptId(receiptId: String): Builder {
|
||||||
|
this.receiptId = receiptId
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun build(): AndroidConfig {
|
||||||
|
return AndroidConfig(
|
||||||
|
collapseKey = collapseKey,
|
||||||
|
androidUrgency = androidUrgency,
|
||||||
|
category = category,
|
||||||
|
ttl = ttl,
|
||||||
|
biTag = biTag,
|
||||||
|
androidFastAppTargetType = androidFastAppTargetType,
|
||||||
|
data = data,
|
||||||
|
androidNotificationConfig = androidNotificationConfig,
|
||||||
|
receiptId = receiptId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
val validator = Validator()
|
||||||
|
|
||||||
|
fun builder() = Builder()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
package ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto
|
||||||
|
|
||||||
|
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.dto.apns.ApnsHeaders
|
||||||
|
import ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.apns.ApnsHmsOptions
|
||||||
|
import ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.apns.Aps
|
||||||
|
|
||||||
|
internal data class ApnsConfig private constructor(
|
||||||
|
@JsonProperty("hms_options")
|
||||||
|
val apnsHmsOptions: ApnsHmsOptions?,
|
||||||
|
@JsonProperty("apns_headers")
|
||||||
|
val apnsHeaders: ApnsHeaders,
|
||||||
|
val payload: Map<String, Any>,
|
||||||
|
) {
|
||||||
|
|
||||||
|
class Validator {
|
||||||
|
|
||||||
|
fun check(apnsConfig: ApnsConfig) {
|
||||||
|
with(apnsConfig) {
|
||||||
|
apnsHmsOptions?.also { ApnsHmsOptions.validator.check(it) }
|
||||||
|
ApnsHeaders.validator.check(apnsHeaders)
|
||||||
|
|
||||||
|
if (payload[APS_PAYLOAD_KEY] != null) {
|
||||||
|
val aps: Aps? = payload[APS_PAYLOAD_KEY] as Aps?
|
||||||
|
|
||||||
|
aps?.also { Aps.validator.check(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class Builder : Buildable {
|
||||||
|
|
||||||
|
private var apnsHmsOptions: ApnsHmsOptions? = null
|
||||||
|
private var payload: MutableMap<String, Any> = mutableMapOf()
|
||||||
|
private var aps: Aps? = null
|
||||||
|
|
||||||
|
fun setApnsHmsOptions(apnsHmsOptions: ApnsHmsOptions): Builder {
|
||||||
|
this.apnsHmsOptions = apnsHmsOptions
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addPayload(payload: Map<String, Any>): Builder {
|
||||||
|
this.payload.putAll(payload)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setAps(aps: Aps): Builder {
|
||||||
|
this.aps = aps
|
||||||
|
payload[APS_PAYLOAD_KEY] = aps
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun build(apnsHeaders: ApnsHeaders): ApnsConfig {
|
||||||
|
return ApnsConfig(
|
||||||
|
apnsHmsOptions = apnsHmsOptions,
|
||||||
|
apnsHeaders = apnsHeaders,
|
||||||
|
payload = payload
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
const val APS_PAYLOAD_KEY = "aps"
|
||||||
|
|
||||||
|
val validator = Validator()
|
||||||
|
|
||||||
|
fun builder() = Builder()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,136 @@
|
||||||
|
package ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import ru.touchin.push.message.provider.hpk.base.builders.Buildable
|
||||||
|
|
||||||
|
internal data class Message private constructor(
|
||||||
|
/** Custom message payload. Map of key-values */
|
||||||
|
val data: String?,
|
||||||
|
/** Notification message content. */
|
||||||
|
@JsonProperty("notification")
|
||||||
|
val notification: Notification?,
|
||||||
|
/** Android message push control. */
|
||||||
|
@JsonProperty("android")
|
||||||
|
val androidConfig: AndroidConfig?,
|
||||||
|
@JsonProperty("apns")
|
||||||
|
val apnsConfig: ApnsConfig?,
|
||||||
|
@JsonProperty("webpush")
|
||||||
|
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. */
|
||||||
|
val topic: String?,
|
||||||
|
val condition: String?,
|
||||||
|
) {
|
||||||
|
|
||||||
|
class Validator {
|
||||||
|
|
||||||
|
fun check(message: Message) {
|
||||||
|
with(message) {
|
||||||
|
require(
|
||||||
|
arrayOf(
|
||||||
|
!token.isNullOrEmpty(),
|
||||||
|
!topic.isNullOrBlank(),
|
||||||
|
!condition.isNullOrBlank(),
|
||||||
|
).count { it } == MAX_TYPES_OF_DELIVERY
|
||||||
|
) { "Exactly one of token, topic or condition must be specified" }
|
||||||
|
|
||||||
|
if (token != null) {
|
||||||
|
require(
|
||||||
|
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) }
|
||||||
|
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 {
|
||||||
|
|
||||||
|
private var data: String? = null
|
||||||
|
private var notification: Notification? = null
|
||||||
|
private var androidConfig: AndroidConfig? = 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
|
||||||
|
|
||||||
|
fun setData(data: String): Builder {
|
||||||
|
this.data = data
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setNotification(notification: Notification): Builder {
|
||||||
|
this.notification = notification
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setAndroidConfig(androidConfig: AndroidConfig): Builder {
|
||||||
|
this.androidConfig = androidConfig
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setApns(apnsConfig: ApnsConfig): Builder {
|
||||||
|
this.apnsConfig = apnsConfig
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setWebpush(webPushConfig: WebPushConfig): Builder {
|
||||||
|
this.webPushConfig = webPushConfig
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addToken(vararg token: String): Builder {
|
||||||
|
this.token.addAll(token)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setTopic(topic: String): Builder {
|
||||||
|
this.topic = topic
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setCondition(condition: String): Builder {
|
||||||
|
this.condition = condition
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun build(): Message {
|
||||||
|
return Message(
|
||||||
|
data = data,
|
||||||
|
notification = notification,
|
||||||
|
androidConfig = androidConfig,
|
||||||
|
apnsConfig = apnsConfig,
|
||||||
|
webPushConfig = webPushConfig,
|
||||||
|
topic = topic,
|
||||||
|
condition = condition,
|
||||||
|
token = token.takeIf(Collection<*>::isNotEmpty),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
val validator = Validator()
|
||||||
|
|
||||||
|
fun builder() = Builder()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
package ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto
|
||||||
|
|
||||||
|
import ru.touchin.push.message.provider.hpk.base.builders.Buildable
|
||||||
|
|
||||||
|
internal data class Notification private constructor(
|
||||||
|
/** Title for notification. Must be specified here or in [AndroidNotificationConfig.title] */
|
||||||
|
val title: String?,
|
||||||
|
/** Text body for notification. Must be specified here or in [AndroidNotificationConfig.body] */
|
||||||
|
val body: String?,
|
||||||
|
/** Url of image */
|
||||||
|
val image: String?,
|
||||||
|
) {
|
||||||
|
|
||||||
|
class Validator {
|
||||||
|
|
||||||
|
fun check(notification: Notification) {
|
||||||
|
with(notification) {
|
||||||
|
if (image != null) {
|
||||||
|
require(
|
||||||
|
image.startsWith(HTTPS_URL_PATTERN)
|
||||||
|
) { "image's url should start with $HTTPS_URL_PATTERN" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
|
||||||
|
const val HTTPS_URL_PATTERN: String = "https"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class Builder : Buildable {
|
||||||
|
|
||||||
|
private var title: String? = null
|
||||||
|
private var body: String? = null
|
||||||
|
private var image: String? = null
|
||||||
|
|
||||||
|
fun setTitle(title: String): Builder {
|
||||||
|
this.title = title
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setBody(body: String): Builder {
|
||||||
|
this.body = body
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setImage(image: String): Builder {
|
||||||
|
this.image = image
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun build(): Notification {
|
||||||
|
return Notification(
|
||||||
|
title = title,
|
||||||
|
body = body,
|
||||||
|
image = image
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
val validator = Validator()
|
||||||
|
|
||||||
|
fun builder() = Builder()
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
package ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto
|
||||||
|
|
||||||
|
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.dto.web.WebHmsOptions
|
||||||
|
import ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.web.WebNotification
|
||||||
|
import ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.web.WebPushHeaders
|
||||||
|
|
||||||
|
internal data class WebPushConfig private constructor(
|
||||||
|
@JsonProperty("headers")
|
||||||
|
val webPushHeaders: WebPushHeaders?,
|
||||||
|
val data: String?,
|
||||||
|
@JsonProperty("notification")
|
||||||
|
val webNotification: WebNotification?,
|
||||||
|
@JsonProperty("hms_options")
|
||||||
|
val webHmsOptions: WebHmsOptions?,
|
||||||
|
) {
|
||||||
|
|
||||||
|
class Validator {
|
||||||
|
|
||||||
|
fun check(webPushConfig: WebPushConfig) {
|
||||||
|
with(webPushConfig) {
|
||||||
|
webPushHeaders?.let { WebPushHeaders.validator.check(it) }
|
||||||
|
webNotification?.let { WebNotification.validator.check(it) }
|
||||||
|
webHmsOptions?.let { WebHmsOptions.validator.check(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class Builder : Buildable {
|
||||||
|
|
||||||
|
private var webPushHeaders: WebPushHeaders? = null
|
||||||
|
private var data: String? = 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 = webPushHeaders,
|
||||||
|
data = data,
|
||||||
|
webNotification = webNotification,
|
||||||
|
webHmsOptions = webHmsOptions,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
val validator = Validator()
|
||||||
|
|
||||||
|
fun builder() = Builder()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
package ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.android
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import ru.touchin.push.message.provider.hpk.base.builders.Buildable
|
||||||
|
|
||||||
|
internal data class AndroidBadgeNotification private constructor(
|
||||||
|
/** Accumulative badge number. */
|
||||||
|
val addNum: Short?,
|
||||||
|
/** Full path of the app entry activity class. */
|
||||||
|
@JsonProperty("class")
|
||||||
|
val clazz: String,
|
||||||
|
/** Badge number. Overrides [addNum]. */
|
||||||
|
val setNum: Short?,
|
||||||
|
) {
|
||||||
|
|
||||||
|
class Validator {
|
||||||
|
|
||||||
|
fun check(androidBadgeNotification: AndroidBadgeNotification) {
|
||||||
|
with(androidBadgeNotification) {
|
||||||
|
if (addNum != null) {
|
||||||
|
require(
|
||||||
|
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 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 {
|
||||||
|
|
||||||
|
private var addNum: Short? = null
|
||||||
|
private var setNum: Short? = null
|
||||||
|
|
||||||
|
fun setAddNum(addNum: Short): Builder {
|
||||||
|
this.addNum = addNum
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setSetNum(setNum: Short): Builder {
|
||||||
|
this.setNum = setNum
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun build(badgeClass: String): AndroidBadgeNotification {
|
||||||
|
return AndroidBadgeNotification(
|
||||||
|
addNum = addNum,
|
||||||
|
clazz = badgeClass,
|
||||||
|
setNum = setNum,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
val validator = Validator()
|
||||||
|
|
||||||
|
fun builder() = Builder()
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
package ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.android
|
||||||
|
|
||||||
|
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.android.AndroidActionType
|
||||||
|
import ru.touchin.push.message.provider.hpk.clients.hms_hpk.enums.android.AndroidIntentType
|
||||||
|
|
||||||
|
internal data class AndroidButton private constructor(
|
||||||
|
/** Button name. */
|
||||||
|
val name: String,
|
||||||
|
/** Button action. */
|
||||||
|
@JsonProperty("action_type")
|
||||||
|
val androidActionType: AndroidActionType,
|
||||||
|
/** Method of opening a custom app page. */
|
||||||
|
@JsonProperty("intent_type")
|
||||||
|
val androidIntentType: AndroidIntentType?,
|
||||||
|
val intent: String?,
|
||||||
|
/** Map of key-values. */
|
||||||
|
val data: String?,
|
||||||
|
) {
|
||||||
|
|
||||||
|
class Validator {
|
||||||
|
|
||||||
|
fun check(androidButton: AndroidButton) {
|
||||||
|
with(androidButton) {
|
||||||
|
require(
|
||||||
|
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 <= 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 {
|
||||||
|
|
||||||
|
private var intent: String? = null
|
||||||
|
private var data: String? = null
|
||||||
|
|
||||||
|
fun setIntent(intent: String): Builder {
|
||||||
|
this.intent = intent
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setData(data: String): Builder {
|
||||||
|
this.data = data
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun build(name: String, androidActionType: AndroidActionType, androidIntentType: AndroidIntentType): AndroidButton {
|
||||||
|
return AndroidButton(
|
||||||
|
name = name,
|
||||||
|
androidActionType = androidActionType,
|
||||||
|
androidIntentType = androidIntentType,
|
||||||
|
intent = intent,
|
||||||
|
data = data
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
val validator = Validator()
|
||||||
|
|
||||||
|
fun builder() = Builder()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
package ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.android
|
||||||
|
|
||||||
|
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.android.AndroidClickActionType
|
||||||
|
|
||||||
|
internal data class AndroidClickAction private constructor(
|
||||||
|
/** Message tapping action type. */
|
||||||
|
@JsonProperty("type")
|
||||||
|
val androidClickActionType: AndroidClickActionType,
|
||||||
|
val intent: String?,
|
||||||
|
/** URL to be opened. */
|
||||||
|
val url: String?,
|
||||||
|
/** Action corresponding to the activity of the page to be opened when the custom app page is opened through the action. */
|
||||||
|
val action: String?,
|
||||||
|
) {
|
||||||
|
|
||||||
|
class Validator {
|
||||||
|
|
||||||
|
fun check(androidClickAction: AndroidClickAction) {
|
||||||
|
with(androidClickAction) {
|
||||||
|
when (androidClickActionType) {
|
||||||
|
AndroidClickActionType.CUSTOMIZE_ACTION -> require(
|
||||||
|
!intent.isNullOrBlank() || !action.isNullOrBlank()
|
||||||
|
) { "intent or action is required when click type is $androidClickActionType" }
|
||||||
|
|
||||||
|
AndroidClickActionType.OPEN_URL -> {
|
||||||
|
require(!url.isNullOrBlank()) { "url is required when click type is $androidClickActionType" }
|
||||||
|
require(url.startsWith(HTTPS_PATTERN_START)) { "url must start with $HTTPS_PATTERN_START" }
|
||||||
|
}
|
||||||
|
|
||||||
|
AndroidClickActionType.OPEN_APP -> {
|
||||||
|
// no verification
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
|
||||||
|
const val HTTPS_PATTERN_START = "https"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class Builder : Buildable {
|
||||||
|
|
||||||
|
private var intent: String? = null
|
||||||
|
private var url: String? = null
|
||||||
|
private var richResource: String? = null
|
||||||
|
private var action: String? = null
|
||||||
|
|
||||||
|
fun setIntent(intent: String): Builder {
|
||||||
|
this.intent = intent
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setUrl(url: String): Builder {
|
||||||
|
this.url = url
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setRichResource(richResource: String): Builder {
|
||||||
|
this.richResource = richResource
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setAction(action: String): Builder {
|
||||||
|
this.action = action
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun build(androidClickActionType: AndroidClickActionType): AndroidClickAction {
|
||||||
|
return AndroidClickAction(
|
||||||
|
androidClickActionType = androidClickActionType,
|
||||||
|
intent = intent.takeIf { androidClickActionType == AndroidClickActionType.CUSTOMIZE_ACTION },
|
||||||
|
action = action.takeIf { androidClickActionType == AndroidClickActionType.CUSTOMIZE_ACTION },
|
||||||
|
url = url.takeIf { androidClickActionType == AndroidClickActionType.OPEN_URL },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
val validator = Validator()
|
||||||
|
|
||||||
|
fun builder() = Builder()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
package ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.android
|
||||||
|
|
||||||
|
import ru.touchin.push.message.provider.hpk.base.builders.Buildable
|
||||||
|
|
||||||
|
internal data class AndroidColor private constructor(
|
||||||
|
/** Alpha setting of the RGB color.*/
|
||||||
|
val alpha: Float,
|
||||||
|
/** Red setting of the RGB color. */
|
||||||
|
val red: Float,
|
||||||
|
/** Green setting of the RGB color. */
|
||||||
|
val green: Float,
|
||||||
|
/** Green setting of the RGB color. */
|
||||||
|
val blue: Float,
|
||||||
|
) {
|
||||||
|
|
||||||
|
class Validator {
|
||||||
|
|
||||||
|
fun check(androidColor: AndroidColor) {
|
||||||
|
with(androidColor) {
|
||||||
|
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.0f
|
||||||
|
private const val ONE: Float = 1.0f
|
||||||
|
val COLOR_RANGE_CONSTRAINT = ZERO..ONE
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class Builder : Buildable {
|
||||||
|
|
||||||
|
private var alpha: Float = 1.0f
|
||||||
|
private var red: Float = 0.0f
|
||||||
|
private var green: Float = 0.0f
|
||||||
|
private var blue: Float = 0.0f
|
||||||
|
|
||||||
|
fun setAlpha(alpha: Float): Builder {
|
||||||
|
this.alpha = alpha
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setRed(red: Float): Builder {
|
||||||
|
this.red = red
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setGreen(green: Float): Builder {
|
||||||
|
this.green = green
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setBlue(blue: Float): Builder {
|
||||||
|
this.blue = blue
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun build(): AndroidColor {
|
||||||
|
return AndroidColor(
|
||||||
|
alpha = alpha,
|
||||||
|
red = red,
|
||||||
|
green = green,
|
||||||
|
blue = blue
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
val validator = Validator()
|
||||||
|
|
||||||
|
fun builder() = Builder()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
package ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.android
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import ru.touchin.push.message.provider.hpk.base.builders.Buildable
|
||||||
|
|
||||||
|
internal data class AndroidLightSettings private constructor(
|
||||||
|
/** Breathing light color. */
|
||||||
|
@JsonProperty("color")
|
||||||
|
val androidColor: AndroidColor,
|
||||||
|
/** Interval when a breathing light is on */
|
||||||
|
val lightOnDuration: String,
|
||||||
|
/** Interval when a breathing light is off */
|
||||||
|
val lightOffDuration: String,
|
||||||
|
) {
|
||||||
|
|
||||||
|
class Validator {
|
||||||
|
|
||||||
|
fun check(androidLightSettings: AndroidLightSettings) {
|
||||||
|
with(androidLightSettings) {
|
||||||
|
AndroidColor.validator.check(androidColor)
|
||||||
|
|
||||||
|
require(
|
||||||
|
lightOnDuration.matches(LIGHT_DURATION_PATTERN)
|
||||||
|
) { "light_on_duration pattern is wrong" }
|
||||||
|
require(
|
||||||
|
lightOffDuration.matches(LIGHT_DURATION_PATTERN)
|
||||||
|
) { "light_off_duration pattern is wrong" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
|
||||||
|
val LIGHT_DURATION_PATTERN: Regex = Regex("\\d+|\\d+[sS]|\\d+.\\d{1,9}|\\d+.\\d{1,9}[sS]")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class Builder : Buildable {
|
||||||
|
|
||||||
|
fun build(
|
||||||
|
color: AndroidColor,
|
||||||
|
lightOnDuration: String,
|
||||||
|
lightOffDuration: String
|
||||||
|
): AndroidLightSettings {
|
||||||
|
return AndroidLightSettings(
|
||||||
|
androidColor = color,
|
||||||
|
lightOnDuration = lightOnDuration,
|
||||||
|
lightOffDuration = lightOffDuration,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
val validator = Validator()
|
||||||
|
|
||||||
|
fun builder() = Builder()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,423 @@
|
||||||
|
package ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.android
|
||||||
|
|
||||||
|
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.dto.Notification
|
||||||
|
import ru.touchin.push.message.provider.hpk.clients.hms_hpk.enums.android.AndroidImportance
|
||||||
|
import ru.touchin.push.message.provider.hpk.clients.hms_hpk.enums.android.AndroidStyleType
|
||||||
|
import ru.touchin.push.message.provider.hpk.clients.hms_hpk.enums.android.AndroidVisibility
|
||||||
|
|
||||||
|
internal data class AndroidNotificationConfig private constructor(
|
||||||
|
/**
|
||||||
|
* Title of an Android notification message.
|
||||||
|
* If the title parameter is set, the value of the [Notification.title] field is overwritten.
|
||||||
|
* */
|
||||||
|
val title: String?,
|
||||||
|
/**
|
||||||
|
* Body of an Android notification message.
|
||||||
|
* If the body parameter is set, the value of the [Notification.body] field is overwritten.
|
||||||
|
* */
|
||||||
|
val body: String?,
|
||||||
|
/**
|
||||||
|
* Custom app icon on the left of a notification message.
|
||||||
|
*/
|
||||||
|
val icon: String?,
|
||||||
|
/** Custom notification bar button color. */
|
||||||
|
val color: String?,
|
||||||
|
val sound: String?,
|
||||||
|
/** Indicates whether to use the default ringtone. */
|
||||||
|
val defaultSound: Boolean,
|
||||||
|
/**
|
||||||
|
* Message tag.
|
||||||
|
* Messages that use the same message tag in the same app will be overwritten by the latest message.
|
||||||
|
* */
|
||||||
|
val tag: String?,
|
||||||
|
@JsonProperty("click_action")
|
||||||
|
val androidClickAction: AndroidClickAction?,
|
||||||
|
val bodyLocKey: String?,
|
||||||
|
val bodyLocArgs: Collection<String>?,
|
||||||
|
val titleLocKey: String?,
|
||||||
|
val titleLocArgs: Collection<String>?,
|
||||||
|
val multiLangKey: Map<String, String>?,
|
||||||
|
/** Custom channel for displaying notification messages. */
|
||||||
|
val channelId: String?,
|
||||||
|
/** Brief description of a notification message to an Android app. */
|
||||||
|
val notifySummary: String?,
|
||||||
|
/** URL of the custom small image on the right of a notification message. */
|
||||||
|
val image: String?,
|
||||||
|
/** Notification bar style. */
|
||||||
|
@JsonProperty("style")
|
||||||
|
val androidStyleType: AndroidStyleType?,
|
||||||
|
/** Android notification message title in large text style. */
|
||||||
|
val bigTitle: String?,
|
||||||
|
val bigBody: String?,
|
||||||
|
/**
|
||||||
|
* Unique notification ID of a message.
|
||||||
|
* If a message does not contain the ID or the ID is -1, the NC will generate a unique ID for the message.
|
||||||
|
* Different notification messages can use the same notification ID, so that new messages can overwrite old messages.
|
||||||
|
* */
|
||||||
|
val notifyId: Int?,
|
||||||
|
/**
|
||||||
|
* Message group.
|
||||||
|
* For example, if 10 messages that contain the same value of group are sent to a device,
|
||||||
|
* the device displays only the latest message and the total number of messages received in the group,
|
||||||
|
* but does not display these 10 messages.
|
||||||
|
*/
|
||||||
|
val group: String?,
|
||||||
|
@JsonProperty("badge")
|
||||||
|
val androidBadgeNotification: AndroidBadgeNotification? = null,
|
||||||
|
val autoCancel: Boolean,
|
||||||
|
/**
|
||||||
|
* Time when Android notification messages are delivered, in the UTC timestamp format.
|
||||||
|
* If you send multiple messages at the same time,
|
||||||
|
* they will be sorted based on this value and displayed in the Android notification panel.
|
||||||
|
* Example: 2014-10-02T15:01:23.045123456Z
|
||||||
|
*/
|
||||||
|
@JsonProperty("when")
|
||||||
|
val sendAt: String?,
|
||||||
|
val localOnly: Boolean? = null,
|
||||||
|
/**
|
||||||
|
* Android notification message priority, which determines the message notification behavior of a user device.
|
||||||
|
*/
|
||||||
|
@JsonProperty("importance")
|
||||||
|
val androidImportance: AndroidImportance?,
|
||||||
|
/** Indicates whether to use the default vibration mode. */
|
||||||
|
val useDefaultVibrate: Boolean,
|
||||||
|
/** Indicates whether to use the default breathing light. */
|
||||||
|
val useDefaultLight: Boolean,
|
||||||
|
val vibrateConfig: Collection<String>?,
|
||||||
|
/** Android notification message visibility. */
|
||||||
|
@JsonProperty("visibility")
|
||||||
|
val androidVisibility: AndroidVisibility?,
|
||||||
|
@JsonProperty("light_settings")
|
||||||
|
val androidLightSettings: AndroidLightSettings?,
|
||||||
|
/**
|
||||||
|
* Indicates whether to display notification messages in the NC when your app is running in the foreground.
|
||||||
|
* If this parameter is not set, the default value true will be used,
|
||||||
|
* indicating that notification messages will be displayed in the NC when your app runs in the foreground.
|
||||||
|
* */
|
||||||
|
val foregroundShow: Boolean,
|
||||||
|
val inboxContent: Collection<String>?,
|
||||||
|
@JsonProperty("buttons")
|
||||||
|
val androidButtons: Collection<AndroidButton>?,
|
||||||
|
/** ID of the user-app relationship. */
|
||||||
|
val profileId: String?,
|
||||||
|
) {
|
||||||
|
|
||||||
|
class Validator {
|
||||||
|
|
||||||
|
@Suppress("ComplexMethod")
|
||||||
|
fun check(androidNotificationConfig: AndroidNotificationConfig, notification: Notification?) {
|
||||||
|
with(androidNotificationConfig) {
|
||||||
|
androidBadgeNotification?.let { AndroidBadgeNotification.validator.check(it) }
|
||||||
|
androidLightSettings?.also { AndroidLightSettings.validator.check(it) }
|
||||||
|
androidClickAction?.also { AndroidClickAction.validator.check(it) }
|
||||||
|
|
||||||
|
require(!notification?.title.isNullOrBlank() || !title.isNullOrBlank()) { "title should be set" }
|
||||||
|
require(!notification?.body.isNullOrBlank() || !body.isNullOrBlank()) { "body should be set" }
|
||||||
|
|
||||||
|
if (!color.isNullOrBlank()) {
|
||||||
|
require(color.matches(COLOR_PATTERN)) { "Wrong color format, color must be in the form #RRGGBB" }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!image.isNullOrBlank()) {
|
||||||
|
require(image.startsWith(HTTPS_URL_PATTERN)) { "notifyIcon must start with $HTTPS_URL_PATTERN" }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (androidStyleType != null) {
|
||||||
|
when (androidStyleType) {
|
||||||
|
AndroidStyleType.DEFAULT -> {
|
||||||
|
// no verification
|
||||||
|
}
|
||||||
|
|
||||||
|
AndroidStyleType.BIG_TEXT -> {
|
||||||
|
require(
|
||||||
|
!bigTitle.isNullOrBlank() && !bigBody.isNullOrBlank()
|
||||||
|
) { "title and body are required when style is $androidStyleType" }
|
||||||
|
}
|
||||||
|
|
||||||
|
AndroidStyleType.INBOX -> {
|
||||||
|
require(
|
||||||
|
!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 <= PROFILE_ID_MAX_LENGTH
|
||||||
|
) { "profileId length cannot exceed $PROFILE_ID_MAX_LENGTH characters" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
|
||||||
|
val COLOR_PATTERN: Regex = Regex("^#[0-9a-fA-F]{6}$")
|
||||||
|
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 var body: String? = null
|
||||||
|
private var icon: String? = null
|
||||||
|
private var color: String? = null
|
||||||
|
private var sound: String? = null
|
||||||
|
private var defaultSound = false
|
||||||
|
private var tag: String? = null
|
||||||
|
private var bodyLocKey: String? = null
|
||||||
|
private val bodyLocArgs: MutableList<String> = mutableListOf()
|
||||||
|
private var titleLocKey: String? = null
|
||||||
|
private val titleLocArgs: MutableList<String> = mutableListOf()
|
||||||
|
private var multiLangkey: Map<String, String>? = null
|
||||||
|
private var channelId: String? = null
|
||||||
|
private var notifySummary: String? = null
|
||||||
|
private var image: String? = null
|
||||||
|
private var androidStyleType: AndroidStyleType? = null
|
||||||
|
private var bigTitle: String? = null
|
||||||
|
private var bigBody: String? = null
|
||||||
|
private var notifyId: Int? = null
|
||||||
|
private var group: String? = null
|
||||||
|
private var androidBadgeNotification: AndroidBadgeNotification? = null
|
||||||
|
private var autoCancel = true
|
||||||
|
private var sendAt: String? = null
|
||||||
|
private var androidImportance: AndroidImportance? = null
|
||||||
|
private var useDefaultVibrate = false
|
||||||
|
private var useDefaultLight = false
|
||||||
|
private val vibrateConfig: MutableList<String> = mutableListOf()
|
||||||
|
private var androidVisibility: AndroidVisibility? = null
|
||||||
|
private var androidLightSettings: AndroidLightSettings? = null
|
||||||
|
private var foregroundShow = false
|
||||||
|
private val inboxContent: MutableList<String> = mutableListOf()
|
||||||
|
private val androidButtons: MutableList<AndroidButton> = mutableListOf()
|
||||||
|
private var profileId: String? = null
|
||||||
|
|
||||||
|
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 setColor(color: String): Builder {
|
||||||
|
this.color = color
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setSound(sound: String): Builder {
|
||||||
|
this.sound = sound
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setDefaultSound(defaultSound: Boolean): Builder {
|
||||||
|
this.defaultSound = defaultSound
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setTag(tag: String): Builder {
|
||||||
|
this.tag = tag
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setBodyLocKey(bodyLocKey: String): Builder {
|
||||||
|
this.bodyLocKey = bodyLocKey
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addBodyLocArgs(vararg arg: String): Builder {
|
||||||
|
bodyLocArgs.addAll(arg)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setTitleLocKey(titleLocKey: String): Builder {
|
||||||
|
this.titleLocKey = titleLocKey
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addTitleLocArgs(vararg args: String): Builder {
|
||||||
|
titleLocArgs.addAll(args)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setMultiLangkey(multiLangkey: Map<String, String>): Builder {
|
||||||
|
this.multiLangkey = multiLangkey
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setChannelId(channelId: String): Builder {
|
||||||
|
this.channelId = channelId
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setNotifySummary(notifySummary: String): Builder {
|
||||||
|
this.notifySummary = notifySummary
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setImage(image: String): Builder {
|
||||||
|
this.image = image
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setStyle(androidStyleType: AndroidStyleType): Builder {
|
||||||
|
this.androidStyleType = androidStyleType
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setBigTitle(bigTitle: String): Builder {
|
||||||
|
this.bigTitle = bigTitle
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setBigBody(bigBody: String): Builder {
|
||||||
|
this.bigBody = bigBody
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setNotifyId(notifyId: Int): Builder {
|
||||||
|
this.notifyId = notifyId
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setGroup(group: String): Builder {
|
||||||
|
this.group = group
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setBadge(androidBadgeNotification: AndroidBadgeNotification): Builder {
|
||||||
|
this.androidBadgeNotification = androidBadgeNotification
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setAutoCancel(autoCancel: Boolean): Builder {
|
||||||
|
this.autoCancel = autoCancel
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sendAt(sendAt: String): Builder {
|
||||||
|
this.sendAt = sendAt
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setImportance(androidImportance: AndroidImportance): Builder {
|
||||||
|
this.androidImportance = androidImportance
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setUseDefaultVibrate(useDefaultVibrate: Boolean): Builder {
|
||||||
|
this.useDefaultVibrate = useDefaultVibrate
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setUseDefaultLight(useDefaultLight: Boolean): Builder {
|
||||||
|
this.useDefaultLight = useDefaultLight
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addVibrateConfig(vararg vibrateTimings: String): Builder {
|
||||||
|
vibrateConfig.addAll(vibrateTimings)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setAndroidVisibility(androidVisibility: AndroidVisibility): Builder {
|
||||||
|
this.androidVisibility = androidVisibility
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setLightSettings(androidLightSettings: AndroidLightSettings): Builder {
|
||||||
|
this.androidLightSettings = androidLightSettings
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setForegroundShow(foregroundShow: Boolean): Builder {
|
||||||
|
this.foregroundShow = foregroundShow
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addInboxContent(vararg inboxContent: String): Builder {
|
||||||
|
this.inboxContent.addAll(inboxContent)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addButton(vararg androidButton: AndroidButton): Builder {
|
||||||
|
androidButtons.addAll(androidButton)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setProfileId(profileId: String): Builder {
|
||||||
|
this.profileId = profileId
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun build(
|
||||||
|
androidClickAction: AndroidClickAction,
|
||||||
|
): AndroidNotificationConfig {
|
||||||
|
return AndroidNotificationConfig(
|
||||||
|
title = title,
|
||||||
|
body = body,
|
||||||
|
icon = icon,
|
||||||
|
color = color,
|
||||||
|
sound = sound,
|
||||||
|
defaultSound = defaultSound,
|
||||||
|
tag = tag,
|
||||||
|
androidClickAction = androidClickAction,
|
||||||
|
bodyLocKey = bodyLocKey,
|
||||||
|
bodyLocArgs = bodyLocArgs.takeIf(Collection<*>::isNotEmpty),
|
||||||
|
titleLocKey = titleLocKey,
|
||||||
|
titleLocArgs = titleLocArgs.takeIf(Collection<*>::isNotEmpty),
|
||||||
|
multiLangKey = multiLangkey,
|
||||||
|
channelId = channelId,
|
||||||
|
notifySummary = notifySummary,
|
||||||
|
image = image,
|
||||||
|
androidStyleType = androidStyleType,
|
||||||
|
bigTitle = bigTitle,
|
||||||
|
bigBody = bigBody,
|
||||||
|
notifyId = notifyId,
|
||||||
|
group = group,
|
||||||
|
androidBadgeNotification = androidBadgeNotification,
|
||||||
|
autoCancel = autoCancel,
|
||||||
|
sendAt = sendAt,
|
||||||
|
androidImportance = androidImportance,
|
||||||
|
useDefaultVibrate = useDefaultVibrate,
|
||||||
|
useDefaultLight = useDefaultLight,
|
||||||
|
vibrateConfig = vibrateConfig.takeIf(Collection<*>::isNotEmpty),
|
||||||
|
androidVisibility = androidVisibility,
|
||||||
|
androidLightSettings = androidLightSettings,
|
||||||
|
foregroundShow = foregroundShow,
|
||||||
|
inboxContent = inboxContent.takeIf(Collection<*>::isNotEmpty),
|
||||||
|
androidButtons = androidButtons.takeIf(Collection<*>::isNotEmpty),
|
||||||
|
profileId = profileId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
val validator = Validator()
|
||||||
|
|
||||||
|
fun builder() = Builder()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,107 @@
|
||||||
|
package ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.apns
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.PropertyNamingStrategies
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonNaming
|
||||||
|
import ru.touchin.push.message.provider.hpk.base.builders.Buildable
|
||||||
|
|
||||||
|
@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy::class)
|
||||||
|
internal data class ApnsAlert private constructor(
|
||||||
|
val title: String?,
|
||||||
|
val body: String?,
|
||||||
|
val titleLocKey: String?,
|
||||||
|
val titleLocArgs: Collection<String>?,
|
||||||
|
val actionLocKey: String?,
|
||||||
|
val locKey: String?,
|
||||||
|
val locArgs: Collection<String>?,
|
||||||
|
val launchImage: String?,
|
||||||
|
) {
|
||||||
|
|
||||||
|
class Validator {
|
||||||
|
|
||||||
|
fun check(apnsAlert: ApnsAlert) {
|
||||||
|
with(apnsAlert) {
|
||||||
|
if (!locArgs.isNullOrEmpty()) {
|
||||||
|
require(!locKey.isNullOrBlank()) { "locKey is required when specifying locArgs" }
|
||||||
|
}
|
||||||
|
if (!titleLocArgs.isNullOrEmpty()) {
|
||||||
|
require(!titleLocKey.isNullOrBlank()) { "titleLocKey is required when specifying titleLocArgs" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class Builder : Buildable {
|
||||||
|
|
||||||
|
private var title: String? = null
|
||||||
|
private var body: String? = null
|
||||||
|
private var titleLocKey: String? = null
|
||||||
|
private val titleLocArgs: MutableList<String> = mutableListOf()
|
||||||
|
private var actionLocKey: String? = null
|
||||||
|
private var locKey: String? = null
|
||||||
|
private val locArgs: MutableList<String> = mutableListOf()
|
||||||
|
private var launchImage: String? = null
|
||||||
|
|
||||||
|
fun setTitle(title: String): Builder {
|
||||||
|
this.title = title
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setBody(body: String): Builder {
|
||||||
|
this.body = body
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setTitleLocKey(titleLocKey: String): Builder {
|
||||||
|
this.titleLocKey = titleLocKey
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setAddTitleLocArg(vararg titleLocArg: String): Builder {
|
||||||
|
titleLocArgs.addAll(titleLocArg)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setActionLocKey(actionLocKey: String): Builder {
|
||||||
|
this.actionLocKey = actionLocKey
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setLocKey(locKey: String): Builder {
|
||||||
|
this.locKey = locKey
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addAllLocArgs(vararg locArgs: String): Builder {
|
||||||
|
this.locArgs.addAll(locArgs)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setLaunchImage(launchImage: String): Builder {
|
||||||
|
this.launchImage = launchImage
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun build(): ApnsAlert {
|
||||||
|
return ApnsAlert(
|
||||||
|
title = title,
|
||||||
|
body = body,
|
||||||
|
titleLocKey = titleLocKey,
|
||||||
|
titleLocArgs = titleLocArgs.takeIf(Collection<*>::isNotEmpty),
|
||||||
|
actionLocKey = actionLocKey,
|
||||||
|
locKey = locKey,
|
||||||
|
locArgs = locArgs.takeIf(Collection<*>::isNotEmpty),
|
||||||
|
launchImage = launchImage,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
val validator = Validator()
|
||||||
|
|
||||||
|
fun builder() = Builder()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
package ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.apns
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import com.fasterxml.jackson.databind.PropertyNamingStrategies
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonNaming
|
||||||
|
import ru.touchin.push.message.provider.hpk.base.builders.Buildable
|
||||||
|
import ru.touchin.push.message.provider.hpk.clients.hms_hpk.enums.apns.ApnsPriority
|
||||||
|
|
||||||
|
@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy::class)
|
||||||
|
internal data class ApnsHeaders private constructor(
|
||||||
|
val authorization: String?,
|
||||||
|
val apnsId: String?,
|
||||||
|
val apnsExpiration: Long?,
|
||||||
|
@JsonProperty("apns-priority")
|
||||||
|
val apnsPriority: ApnsPriority?,
|
||||||
|
val apnsTopic: String?,
|
||||||
|
val apnsCollapseId: String?,
|
||||||
|
) {
|
||||||
|
|
||||||
|
class Validator {
|
||||||
|
|
||||||
|
fun check(apnsHeaders: ApnsHeaders) {
|
||||||
|
with(apnsHeaders) {
|
||||||
|
if (authorization != null) {
|
||||||
|
require(
|
||||||
|
authorization.startsWith(AUTHORIZATION_PATTERN)
|
||||||
|
) { "authorization must start with bearer" }
|
||||||
|
}
|
||||||
|
if (apnsId != null) {
|
||||||
|
require(apnsId.matches(APN_ID_PATTERN)) { "apns-id format error" }
|
||||||
|
}
|
||||||
|
if (apnsCollapseId != null) {
|
||||||
|
require(
|
||||||
|
apnsCollapseId.toByteArray().size < APNS_COLLAPSE_ID_MAX_SIZE
|
||||||
|
) { "Number of apnsCollapseId bytes must be less than $APNS_COLLAPSE_ID_MAX_SIZE" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class Builder : Buildable {
|
||||||
|
|
||||||
|
private var authorization: String? = null
|
||||||
|
private var apnsId: String? = null
|
||||||
|
private var apnsExpiration: Long? = null
|
||||||
|
private var apnsPriority: ApnsPriority? = null
|
||||||
|
private var apnsTopic: String? = null
|
||||||
|
private var apnsCollapseId: String? = null
|
||||||
|
|
||||||
|
fun setAuthorization(authorization: String): Builder {
|
||||||
|
this.authorization = authorization
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setApnsId(apnsId: String): Builder {
|
||||||
|
this.apnsId = apnsId
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setApnsExpiration(apnsExpiration: Long): Builder {
|
||||||
|
this.apnsExpiration = apnsExpiration
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setApnsPriority(apnsPriority: ApnsPriority): Builder {
|
||||||
|
this.apnsPriority = apnsPriority
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setApnsTopic(apnsTopic: String): Builder {
|
||||||
|
this.apnsTopic = apnsTopic
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setApnsCollapseId(apnsCollapseId: String): Builder {
|
||||||
|
this.apnsCollapseId = apnsCollapseId
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun build(): ApnsHeaders {
|
||||||
|
return ApnsHeaders(
|
||||||
|
authorization = authorization,
|
||||||
|
apnsId = apnsId,
|
||||||
|
apnsExpiration = apnsExpiration,
|
||||||
|
apnsPriority = apnsPriority,
|
||||||
|
apnsTopic = apnsTopic,
|
||||||
|
apnsCollapseId = apnsCollapseId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
val validator = Validator()
|
||||||
|
|
||||||
|
fun builder() = Builder()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
package ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.apns
|
||||||
|
|
||||||
|
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.android.AndroidTargetUserType
|
||||||
|
|
||||||
|
internal data class ApnsHmsOptions private constructor(
|
||||||
|
@JsonProperty("target_user_type")
|
||||||
|
val androidTargetUserType: AndroidTargetUserType,
|
||||||
|
) {
|
||||||
|
|
||||||
|
class Validator {
|
||||||
|
|
||||||
|
@Suppress("UNUSED_PARAMETER")
|
||||||
|
fun check(apnsHmsOptions: ApnsHmsOptions) {
|
||||||
|
// no validation
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class Builder : Buildable {
|
||||||
|
|
||||||
|
fun build(androidTargetUserType: AndroidTargetUserType): ApnsHmsOptions {
|
||||||
|
return ApnsHmsOptions(
|
||||||
|
androidTargetUserType = androidTargetUserType,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
val validator = Validator()
|
||||||
|
|
||||||
|
fun builder() = Builder()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
package ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.apns
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import com.fasterxml.jackson.databind.PropertyNamingStrategies
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonNaming
|
||||||
|
import ru.touchin.push.message.provider.hpk.base.builders.Buildable
|
||||||
|
|
||||||
|
@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy::class)
|
||||||
|
internal class Aps private constructor(
|
||||||
|
@JsonProperty("alert")
|
||||||
|
val apnsAlert: ApnsAlert?,
|
||||||
|
val badge: Int?,
|
||||||
|
val sound: String?,
|
||||||
|
val contentAvailable: Int?,
|
||||||
|
val category: String?,
|
||||||
|
val threadId: String?,
|
||||||
|
) {
|
||||||
|
|
||||||
|
class Validator {
|
||||||
|
|
||||||
|
fun check(aps: Aps) {
|
||||||
|
with(aps) {
|
||||||
|
if (apnsAlert != null) {
|
||||||
|
ApnsAlert.validator.check(apnsAlert)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class Builder : Buildable {
|
||||||
|
|
||||||
|
private var apnsAlert: ApnsAlert? = null
|
||||||
|
private var badge: Int? = null
|
||||||
|
private var sound: String? = null
|
||||||
|
private var contentAvailable: Int? = null
|
||||||
|
private var category: String? = null
|
||||||
|
private var threadId: String? = null
|
||||||
|
|
||||||
|
fun setAlert(alert: ApnsAlert): Builder {
|
||||||
|
this.apnsAlert = alert
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setBadge(badge: Int): Builder {
|
||||||
|
this.badge = badge
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setSound(sound: String): Builder {
|
||||||
|
this.sound = sound
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setContentAvailable(contentAvailable: Int): Builder {
|
||||||
|
this.contentAvailable = contentAvailable
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setCategory(category: String): Builder {
|
||||||
|
this.category = category
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setThreadId(threadId: String): Builder {
|
||||||
|
this.threadId = threadId
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun build(): Aps {
|
||||||
|
return Aps(
|
||||||
|
apnsAlert = apnsAlert,
|
||||||
|
badge = badge,
|
||||||
|
sound = sound,
|
||||||
|
contentAvailable = contentAvailable,
|
||||||
|
category = category,
|
||||||
|
threadId = threadId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
val validator = Validator()
|
||||||
|
|
||||||
|
fun builder() = Builder()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
package ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.web
|
||||||
|
|
||||||
|
import ru.touchin.push.message.provider.hpk.base.builders.Buildable
|
||||||
|
|
||||||
|
internal data class WebActions private constructor(
|
||||||
|
val action: String?,
|
||||||
|
val icon: String?,
|
||||||
|
val title: String?,
|
||||||
|
) {
|
||||||
|
|
||||||
|
class Validator {
|
||||||
|
|
||||||
|
fun check() {
|
||||||
|
// no validation
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class Builder : Buildable {
|
||||||
|
|
||||||
|
private var action: String? = null
|
||||||
|
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,
|
||||||
|
icon = icon,
|
||||||
|
title = title,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
val validator = Validator()
|
||||||
|
|
||||||
|
fun builder() = Builder()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
package ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.web
|
||||||
|
|
||||||
|
import ru.touchin.push.message.provider.hpk.base.builders.Buildable
|
||||||
|
import java.net.MalformedURLException
|
||||||
|
import java.net.URL
|
||||||
|
|
||||||
|
internal data class WebHmsOptions private constructor(
|
||||||
|
val link: String?,
|
||||||
|
) {
|
||||||
|
|
||||||
|
class Validator {
|
||||||
|
|
||||||
|
fun check(webHmsOptions: WebHmsOptions) {
|
||||||
|
with(webHmsOptions) {
|
||||||
|
if (!link.isNullOrBlank()) {
|
||||||
|
try {
|
||||||
|
URL(link)
|
||||||
|
} catch (e: MalformedURLException) {
|
||||||
|
require(false) { "Invalid link" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class Builder : Buildable {
|
||||||
|
|
||||||
|
private var link: String? = null
|
||||||
|
|
||||||
|
fun setLink(link: String): Builder {
|
||||||
|
this.link = link
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun build(): WebHmsOptions {
|
||||||
|
return WebHmsOptions(
|
||||||
|
link = link,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
val validator = Validator()
|
||||||
|
|
||||||
|
fun builder() = Builder()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,151 @@
|
||||||
|
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(
|
||||||
|
val title: String?,
|
||||||
|
val body: String?,
|
||||||
|
val icon: String?,
|
||||||
|
val image: String?,
|
||||||
|
val lang: String?,
|
||||||
|
val tag: String?,
|
||||||
|
val badge: 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 webActions: Collection<WebActions>?,
|
||||||
|
) {
|
||||||
|
|
||||||
|
class Validator {
|
||||||
|
|
||||||
|
@Suppress("UNUSED_PARAMETER")
|
||||||
|
fun check(webNotification: WebNotification) {
|
||||||
|
// no verification required
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class Builder : Buildable {
|
||||||
|
|
||||||
|
private var title: String? = null
|
||||||
|
private var body: String? = null
|
||||||
|
private var icon: String? = null
|
||||||
|
private var image: String? = null
|
||||||
|
private var lang: String? = null
|
||||||
|
private var tag: String? = null
|
||||||
|
private var badge: 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 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(
|
||||||
|
title = title,
|
||||||
|
body = body,
|
||||||
|
icon = icon,
|
||||||
|
image = image,
|
||||||
|
lang = lang,
|
||||||
|
tag = tag,
|
||||||
|
badge = badge,
|
||||||
|
webDir = webDir,
|
||||||
|
vibrate = vibrate.takeIf(Collection<*>::isNotEmpty),
|
||||||
|
renotify = renotify,
|
||||||
|
requireInteraction = requireInteraction,
|
||||||
|
silent = silent,
|
||||||
|
timestamp = timestamp,
|
||||||
|
webActions = webActions.takeIf(Collection<*>::isNotEmpty),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
val validator = Validator()
|
||||||
|
|
||||||
|
fun builder() = Builder()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
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?,
|
||||||
|
@JsonProperty("urgency")
|
||||||
|
val webUrgency: WebUrgency?,
|
||||||
|
) {
|
||||||
|
|
||||||
|
class Validator {
|
||||||
|
|
||||||
|
fun check(webpushHeaders: WebPushHeaders) {
|
||||||
|
with(webpushHeaders) {
|
||||||
|
if (ttl != null) {
|
||||||
|
require(ttl.matches(TTL_PATTERN)) { "Invalid ttl format" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
|
||||||
|
val TTL_PATTERN: Regex = Regex("[0-9]+|[0-9]+[sS]")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class Builder : Buildable {
|
||||||
|
|
||||||
|
private var ttl: String? = null
|
||||||
|
private var topic: String? = null
|
||||||
|
private var urgency: WebUrgency? = null
|
||||||
|
|
||||||
|
fun setTtl(ttl: String): Builder {
|
||||||
|
this.ttl = ttl
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setTopic(topic: String): Builder {
|
||||||
|
this.topic = topic
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setUrgency(urgency: WebUrgency): Builder {
|
||||||
|
this.urgency = urgency
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun build(): WebPushHeaders {
|
||||||
|
return WebPushHeaders(
|
||||||
|
ttl = ttl,
|
||||||
|
topic = topic,
|
||||||
|
webUrgency = urgency,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
val validator = Validator()
|
||||||
|
|
||||||
|
fun builder() = Builder()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package ru.touchin.push.message.provider.hpk.clients.hms_hpk.enums.android
|
||||||
|
|
||||||
|
import ru.touchin.push.message.provider.hpk.base.enums.ValueableSerializableEnum
|
||||||
|
|
||||||
|
internal enum class AndroidActionType(
|
||||||
|
override val value: Short
|
||||||
|
) : ValueableSerializableEnum<Short> {
|
||||||
|
|
||||||
|
OPEN_APP_HOME_PAGE(0),
|
||||||
|
OPEN_CUSTOM_APP_PAGE(1),
|
||||||
|
OPEN_WEB_PAGE(2),
|
||||||
|
DELETE_NOTIFICATION_MESSAGE(3),
|
||||||
|
|
||||||
|
/** Only for Huawei devices */
|
||||||
|
SHARE_NOTIFICATION_MESSAGE(4),
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
package ru.touchin.push.message.provider.hpk.clients.hms_hpk.enums.android
|
||||||
|
|
||||||
|
import ru.touchin.push.message.provider.hpk.base.enums.ValueableSerializableEnum
|
||||||
|
|
||||||
|
internal enum class AndroidClickActionType(
|
||||||
|
override val value: Short
|
||||||
|
) : ValueableSerializableEnum<Short> {
|
||||||
|
|
||||||
|
CUSTOMIZE_ACTION(1),
|
||||||
|
OPEN_URL(2),
|
||||||
|
OPEN_APP(3),
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
package ru.touchin.push.message.provider.hpk.clients.hms_hpk.enums.android
|
||||||
|
|
||||||
|
import ru.touchin.push.message.provider.hpk.base.enums.ValueableSerializableEnum
|
||||||
|
|
||||||
|
internal enum class AndroidFastAppTargetType(
|
||||||
|
override val value: Short
|
||||||
|
) : ValueableSerializableEnum<Short> {
|
||||||
|
|
||||||
|
DEVELOPMENT(1),
|
||||||
|
PRODUCTION(2),
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
package ru.touchin.push.message.provider.hpk.clients.hms_hpk.enums.android
|
||||||
|
|
||||||
|
import ru.touchin.push.message.provider.hpk.base.enums.ValueableSerializableEnum
|
||||||
|
|
||||||
|
internal enum class AndroidImportance(
|
||||||
|
override val value: String
|
||||||
|
) : ValueableSerializableEnum<String> {
|
||||||
|
|
||||||
|
LOW("LOW"),
|
||||||
|
NORMAL("NORMAL"),
|
||||||
|
HIGH("HIGH"), // TODO: check if this type is still supported by HMS HPK API
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
package ru.touchin.push.message.provider.hpk.clients.hms_hpk.enums.android
|
||||||
|
|
||||||
|
import ru.touchin.push.message.provider.hpk.base.enums.ValueableSerializableEnum
|
||||||
|
|
||||||
|
enum class AndroidIntentType(
|
||||||
|
override val value: Short
|
||||||
|
) : ValueableSerializableEnum<Short> {
|
||||||
|
|
||||||
|
INTENT(0),
|
||||||
|
ACTION(1),
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
package ru.touchin.push.message.provider.hpk.clients.hms_hpk.enums.android
|
||||||
|
|
||||||
|
import ru.touchin.push.message.provider.hpk.base.enums.ValueableSerializableEnum
|
||||||
|
|
||||||
|
enum class AndroidStyleType(
|
||||||
|
override val value: Short
|
||||||
|
) : ValueableSerializableEnum<Short> {
|
||||||
|
|
||||||
|
DEFAULT(0),
|
||||||
|
BIG_TEXT(1),
|
||||||
|
INBOX(3),
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
package ru.touchin.push.message.provider.hpk.clients.hms_hpk.enums.android
|
||||||
|
|
||||||
|
import ru.touchin.push.message.provider.hpk.base.enums.ValueableSerializableEnum
|
||||||
|
|
||||||
|
internal enum class AndroidTargetUserType(
|
||||||
|
override val value: Short
|
||||||
|
) : ValueableSerializableEnum<Short> {
|
||||||
|
|
||||||
|
TEST_USER(1),
|
||||||
|
FORMAL_USER(2),
|
||||||
|
VOIP_USER(3),
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
package ru.touchin.push.message.provider.hpk.clients.hms_hpk.enums.android
|
||||||
|
|
||||||
|
import ru.touchin.push.message.provider.hpk.base.enums.ValueableSerializableEnum
|
||||||
|
|
||||||
|
internal enum class AndroidTopicOperation(
|
||||||
|
override val value: String
|
||||||
|
) : ValueableSerializableEnum<String> {
|
||||||
|
|
||||||
|
SUBSCRIBE("subscribe"),
|
||||||
|
UNSUBSCRIBE("unsubscribe"),
|
||||||
|
LIST("list"),
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
package ru.touchin.push.message.provider.hpk.clients.hms_hpk.enums.android
|
||||||
|
|
||||||
|
import ru.touchin.push.message.provider.hpk.base.enums.ValueableSerializableEnum
|
||||||
|
|
||||||
|
internal enum class AndroidUrgency(
|
||||||
|
override val value: String
|
||||||
|
) : ValueableSerializableEnum<String> {
|
||||||
|
|
||||||
|
HIGH("HIGH"),
|
||||||
|
NORMAL("NORMAL"),
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
package ru.touchin.push.message.provider.hpk.clients.hms_hpk.enums.android
|
||||||
|
|
||||||
|
import ru.touchin.push.message.provider.hpk.base.enums.ValueableSerializableEnum
|
||||||
|
|
||||||
|
internal enum class AndroidVisibility(
|
||||||
|
override val value: String
|
||||||
|
) : ValueableSerializableEnum<String> {
|
||||||
|
|
||||||
|
/** The visibility is not specified. This value is equivalent to PRIVATE. */
|
||||||
|
VISIBILITY_UNSPECIFIED("VISIBILITY_UNSPECIFIED"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If you have set a lock screen password and enabled Hide notification content under Settings > Notifications,
|
||||||
|
* the content of a received notification message is hidden on the lock screen.
|
||||||
|
* */
|
||||||
|
PRIVATE("PRIVATE"),
|
||||||
|
|
||||||
|
/** The content of a received notification message is displayed on the lock screen. */
|
||||||
|
PUBLIC("PUBLIC"),
|
||||||
|
|
||||||
|
/** A received notification message is not displayed on the lock screen. */
|
||||||
|
SECRET("SECRET"),
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
package ru.touchin.push.message.provider.hpk.clients.hms_hpk.enums.apns
|
||||||
|
|
||||||
|
import ru.touchin.push.message.provider.hpk.base.enums.ValueableSerializableEnum
|
||||||
|
|
||||||
|
enum class ApnsPriority(
|
||||||
|
override val value: Short
|
||||||
|
) : ValueableSerializableEnum<Short> {
|
||||||
|
|
||||||
|
SEND_BY_GROUP(5),
|
||||||
|
SEND_IMMIDIATELY(10),
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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"),
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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"),
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
package ru.touchin.push.message.provider.hpk.clients.hms_hpk.requests
|
||||||
|
|
||||||
|
import ru.touchin.push.message.provider.hpk.clients.hms_hpk.bodies.HmsHpkMessagesSendBody
|
||||||
|
|
||||||
|
internal class HmsHpkMessagesSendRequest(
|
||||||
|
val hmsHpkMessagesSendBody: HmsHpkMessagesSendBody,
|
||||||
|
accessToken: String,
|
||||||
|
) : HmsHpkRequest(
|
||||||
|
accessToken = accessToken,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
package ru.touchin.push.message.provider.hpk.clients.hms_hpk.requests
|
||||||
|
|
||||||
|
internal open class HmsHpkRequest(
|
||||||
|
val accessToken: String,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
package ru.touchin.push.message.provider.hpk.clients.hms_hpk.responses
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.PropertyNamingStrategies
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonNaming
|
||||||
|
|
||||||
|
@JsonNaming(PropertyNamingStrategies.LowerCamelCaseStrategy::class)
|
||||||
|
internal class HmsHpkResponse(
|
||||||
|
/** Result code. */
|
||||||
|
val code: String,
|
||||||
|
/** Result code description. */
|
||||||
|
val msg: String,
|
||||||
|
/** Request ID. */
|
||||||
|
val requestId: String,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
package ru.touchin.push.message.provider.hpk.clients.hms_oauth
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier
|
||||||
|
import org.springframework.http.HttpMethod
|
||||||
|
import org.springframework.http.MediaType
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import org.springframework.web.reactive.function.BodyInserters
|
||||||
|
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.base.clients.dto.ConditionalResponse
|
||||||
|
import ru.touchin.push.message.provider.hpk.clients.hms_oauth.response.HmsOauthErrorResponse
|
||||||
|
import ru.touchin.push.message.provider.hpk.clients.hms_oauth.response.HmsOauthTokenResponse
|
||||||
|
import ru.touchin.push.message.provider.hpk.properties.HpkProperties
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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")
|
||||||
|
private val objectMapper: ObjectMapper,
|
||||||
|
) : ConfigurableWebClient(webClientLogger, webClientBuilder, hpkProperties.webServices.oauth) {
|
||||||
|
|
||||||
|
override fun getObjectMapper(): ObjectMapper = objectMapper
|
||||||
|
|
||||||
|
override fun getWebClient(): WebClient {
|
||||||
|
return getWebClientBuilder(
|
||||||
|
url = webService.url.toString(),
|
||||||
|
)
|
||||||
|
.setTimeouts()
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun token(): ConditionalResponse<HmsOauthTokenResponse, HmsOauthErrorResponse> {
|
||||||
|
return getWebClient()
|
||||||
|
.post()
|
||||||
|
.uri { builder ->
|
||||||
|
builder
|
||||||
|
.path(METHOD_TOKEN)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
|
||||||
|
.body(
|
||||||
|
BodyInserters
|
||||||
|
.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(
|
||||||
|
uri = METHOD_TOKEN,
|
||||||
|
logTags = listOf(),
|
||||||
|
method = HttpMethod.POST,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.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"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package ru.touchin.push.message.provider.hpk.clients.hms_oauth.response
|
||||||
|
|
||||||
|
internal class HmsOauthErrorResponse(
|
||||||
|
val error: Int,
|
||||||
|
val subError: Int,
|
||||||
|
val errorDescription: String,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
package ru.touchin.push.message.provider.hpk.clients.hms_oauth.response
|
||||||
|
|
||||||
|
internal class HmsOauthTokenResponse(
|
||||||
|
val tokenType: String,
|
||||||
|
val accessToken: String,
|
||||||
|
/** Expiration in seconds. */
|
||||||
|
val expiresIn: Long,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
package ru.touchin.push.message.provider.hpk.configurations
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude
|
||||||
|
import com.fasterxml.jackson.databind.DeserializationFeature
|
||||||
|
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.boot.autoconfigure.condition.ConditionalOnMissingBean
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationPropertiesScan
|
||||||
|
import org.springframework.cache.CacheManager
|
||||||
|
import org.springframework.cache.concurrent.ConcurrentMapCache
|
||||||
|
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
|
||||||
|
|
||||||
|
@ComponentScan("ru.touchin.push.message.provider.hpk")
|
||||||
|
@ConfigurationPropertiesScan(basePackages = ["ru.touchin.push.message.provider.hpk"])
|
||||||
|
@Import(value = [PushMessageProviderConfiguration::class])
|
||||||
|
class PushMessageProviderHpkConfiguration {
|
||||||
|
|
||||||
|
@Bean("push-message-provider.hpk.webclient-objectmapper")
|
||||||
|
fun webclientObjectMapper(): ObjectMapper {
|
||||||
|
return jacksonObjectMapper()
|
||||||
|
.registerModule(JavaTimeModule())
|
||||||
|
.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
|
||||||
|
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
|
||||||
|
.setSerializationInclusion(JsonInclude.Include.NON_EMPTY)
|
||||||
|
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean("push-message-provider.hpk.client-objectmapper")
|
||||||
|
fun clientObjectMapper(): ObjectMapper {
|
||||||
|
return jacksonObjectMapper()
|
||||||
|
.registerModule(JavaTimeModule())
|
||||||
|
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
|
||||||
|
.setSerializationInclusion(JsonInclude.Include.NON_EMPTY)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean("push-message-provider.hpk.webclient-cachemanager")
|
||||||
|
@ConditionalOnMissingBean
|
||||||
|
fun cacheManager(): CacheManager {
|
||||||
|
return SimpleCacheManager().also {
|
||||||
|
it.setCaches(
|
||||||
|
listOf(
|
||||||
|
ConcurrentMapCache(HMS_CLIENT_SERVICE_CACHE_KEY)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
package ru.touchin.push.message.provider.hpk.converters
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
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("push-message-provider.hpk.push-message-notification-converter")
|
||||||
|
class PushMessageNotificationConverter {
|
||||||
|
|
||||||
|
internal operator fun invoke(pushMessageNotification: PushMessageNotification): HmsNotification {
|
||||||
|
return HmsNotification.builder()
|
||||||
|
.ifNotNull(pushMessageNotification.imageUrl) { setImage(it) }
|
||||||
|
.ifNotNull(pushMessageNotification.title) { setTitle(it) }
|
||||||
|
.ifNotNull(pushMessageNotification.description) { setBody(it) }
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
package ru.touchin.push.message.provider.hpk.converters
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import ru.touchin.push.message.provider.dto.request.PushTokenMessage
|
||||||
|
import ru.touchin.push.message.provider.hpk.base.extensions.ifNotNull
|
||||||
|
import ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.AndroidConfig
|
||||||
|
import ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.Message
|
||||||
|
import ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.android.AndroidClickAction
|
||||||
|
import ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.android.AndroidNotificationConfig
|
||||||
|
import ru.touchin.push.message.provider.hpk.clients.hms_hpk.enums.android.AndroidClickActionType
|
||||||
|
import kotlin.jvm.Throws
|
||||||
|
|
||||||
|
@Component("push-message-provider.hpk.push-token-message-converter")
|
||||||
|
class PushTokenMessageConverter(
|
||||||
|
private val pushMessageNotificationConverter: PushMessageNotificationConverter,
|
||||||
|
@Qualifier("push-message-provider.hpk.client-objectmapper")
|
||||||
|
private val objectMapper: ObjectMapper,
|
||||||
|
) {
|
||||||
|
|
||||||
|
@Throws(IllegalArgumentException::class)
|
||||||
|
internal operator fun invoke(request: PushTokenMessage): Message {
|
||||||
|
return Message.builder()
|
||||||
|
.addToken(request.token)
|
||||||
|
.ifNotNull(request.data.takeIf(Map<*, *>::isNotEmpty)) { data ->
|
||||||
|
setData(objectMapper.writeValueAsString(data))
|
||||||
|
}
|
||||||
|
.ifNotNull(request.pushMessageNotification) { notification ->
|
||||||
|
setNotification(pushMessageNotificationConverter(notification))
|
||||||
|
}
|
||||||
|
.setupAndroidConfig()
|
||||||
|
.build()
|
||||||
|
.also { Message.validator.check(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Message.Builder.setupAndroidConfig(): Message.Builder {
|
||||||
|
return setAndroidConfig(
|
||||||
|
AndroidConfig.builder()
|
||||||
|
.setAndroidNotificationConfig(
|
||||||
|
AndroidNotificationConfig.builder()
|
||||||
|
.setDefaultSound(USE_DEFAULT_SOUND)
|
||||||
|
.build(AndroidClickAction.builder().build(DEFAULT_ANDROID_CLICK_ACTION_TYPE))
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
|
||||||
|
const val USE_DEFAULT_SOUND = true
|
||||||
|
val DEFAULT_ANDROID_CLICK_ACTION_TYPE = AndroidClickActionType.OPEN_APP
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
package ru.touchin.push.message.provider.hpk.properties
|
||||||
|
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||||
|
import org.springframework.boot.context.properties.ConstructorBinding
|
||||||
|
import java.net.URL
|
||||||
|
import java.time.Duration
|
||||||
|
|
||||||
|
@ConstructorBinding
|
||||||
|
@ConfigurationProperties(prefix = "push-message-provider.hpk")
|
||||||
|
data class HpkProperties(
|
||||||
|
val webServices: WebServices,
|
||||||
|
) {
|
||||||
|
|
||||||
|
class WebServices(
|
||||||
|
val clientId: String,
|
||||||
|
val oauth: Oauth,
|
||||||
|
val hpk: Hpk,
|
||||||
|
)
|
||||||
|
|
||||||
|
class Oauth(
|
||||||
|
val clientSecret: String,
|
||||||
|
url: URL,
|
||||||
|
http: Http,
|
||||||
|
ssl: Ssl?,
|
||||||
|
) : WebService(
|
||||||
|
url = url,
|
||||||
|
http = http,
|
||||||
|
ssl = ssl,
|
||||||
|
)
|
||||||
|
|
||||||
|
class Hpk(
|
||||||
|
url: URL,
|
||||||
|
http: Http,
|
||||||
|
ssl: Ssl?,
|
||||||
|
) : WebService(
|
||||||
|
url = url,
|
||||||
|
http = http,
|
||||||
|
ssl = ssl,
|
||||||
|
)
|
||||||
|
|
||||||
|
open class WebService(
|
||||||
|
val url: URL,
|
||||||
|
val http: Http,
|
||||||
|
val ssl: Ssl?,
|
||||||
|
)
|
||||||
|
|
||||||
|
class Http(
|
||||||
|
val readTimeout: Duration,
|
||||||
|
val writeTimeout: Duration,
|
||||||
|
val connectionTimeout: Duration,
|
||||||
|
)
|
||||||
|
|
||||||
|
class Ssl(
|
||||||
|
val handshakeTimeout: Duration,
|
||||||
|
val notifyFlushTimeout: Duration,
|
||||||
|
val notifyReadTimeout: Duration,
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
package ru.touchin.push.message.provider.hpk.services
|
||||||
|
|
||||||
|
import ru.touchin.push.message.provider.dto.request.PushTokenCheck
|
||||||
|
import ru.touchin.push.message.provider.dto.request.PushTokenMessage
|
||||||
|
import ru.touchin.push.message.provider.enums.PushTokenStatus
|
||||||
|
|
||||||
|
interface HmsHpkClientService {
|
||||||
|
|
||||||
|
fun send(request: PushTokenMessage)
|
||||||
|
|
||||||
|
fun check(request: PushTokenCheck): PushTokenStatus
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
package ru.touchin.push.message.provider.hpk.services
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import ru.touchin.push.message.provider.dto.request.PushTokenCheck
|
||||||
|
import ru.touchin.push.message.provider.dto.request.PushTokenMessage
|
||||||
|
import ru.touchin.push.message.provider.enums.PushTokenStatus
|
||||||
|
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.bodies.HmsHpkMessagesSendBody
|
||||||
|
import ru.touchin.push.message.provider.hpk.clients.hms_hpk.requests.HmsHpkMessagesSendRequest
|
||||||
|
import ru.touchin.push.message.provider.hpk.converters.PushTokenMessageConverter
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class HmsHpkClientServiceImpl(
|
||||||
|
private val hmsHpkWebClient: HmsHpkWebClient,
|
||||||
|
private val hmsOauthClientService: HmsOauthClientService,
|
||||||
|
private val pushTokenMessageConverter: PushTokenMessageConverter,
|
||||||
|
) : HmsHpkClientService {
|
||||||
|
|
||||||
|
override fun send(request: PushTokenMessage) {
|
||||||
|
sendToPushToken(request, dryRun = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun check(request: PushTokenCheck): PushTokenStatus {
|
||||||
|
val validationRequest = PushTokenMessage(
|
||||||
|
token = request.pushToken,
|
||||||
|
pushMessageNotification = null,
|
||||||
|
data = emptyMap()
|
||||||
|
)
|
||||||
|
|
||||||
|
return try {
|
||||||
|
sendToPushToken(validationRequest, dryRun = false)
|
||||||
|
|
||||||
|
PushTokenStatus.VALID
|
||||||
|
} catch (ipte: InvalidPushTokenException) {
|
||||||
|
PushTokenStatus.INVALID
|
||||||
|
} catch (pmpe: PushMessageProviderException) {
|
||||||
|
PushTokenStatus.UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(PushMessageProviderException::class, InvalidPushTokenException::class)
|
||||||
|
private fun sendToPushToken(request: PushTokenMessage, dryRun: Boolean) {
|
||||||
|
val accessToken = hmsOauthClientService.getAccessToken()
|
||||||
|
|
||||||
|
val result = hmsHpkWebClient.messagesSend(
|
||||||
|
HmsHpkMessagesSendRequest(
|
||||||
|
hmsHpkMessagesSendBody = HmsHpkMessagesSendBody(
|
||||||
|
validateOnly = dryRun,
|
||||||
|
message = pushTokenMessageConverter(request),
|
||||||
|
),
|
||||||
|
accessToken = accessToken,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
when (HmsResponseCode.fromCode(result.code)) {
|
||||||
|
HmsResponseCode.SUCCESS -> {
|
||||||
|
// pass result
|
||||||
|
}
|
||||||
|
|
||||||
|
HmsResponseCode.INVALID_TOKEN,
|
||||||
|
HmsResponseCode.INVALID_CLIENT_SECRET -> {
|
||||||
|
throw InvalidPushTokenException()
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
throw PushMessageProviderException(result.msg, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
package ru.touchin.push.message.provider.hpk.services
|
||||||
|
|
||||||
|
import ru.touchin.push.message.provider.hpk.services.dto.AccessToken
|
||||||
|
|
||||||
|
interface HmsOauthAccessTokenCacheService {
|
||||||
|
|
||||||
|
fun put(accessToken: AccessToken)
|
||||||
|
fun get(): AccessToken?
|
||||||
|
fun evict()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
package ru.touchin.push.message.provider.hpk.services
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
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")
|
||||||
|
private val objectMapper: ObjectMapper,
|
||||||
|
private val hpkProperties: HpkProperties,
|
||||||
|
) : HmsOauthAccessTokenCacheService {
|
||||||
|
|
||||||
|
override fun put(accessToken: AccessToken) {
|
||||||
|
getCache()?.put(hpkProperties.webServices.clientId, accessToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun get(): AccessToken? { // TODO: implement synchronization for all threads
|
||||||
|
val cachedValue = getCache()
|
||||||
|
?.get(hpkProperties.webServices.clientId)
|
||||||
|
?.get()
|
||||||
|
?: return null
|
||||||
|
|
||||||
|
val accessToken = safeCast(cachedValue, object : TypeReference<AccessToken>() {})
|
||||||
|
?: return null
|
||||||
|
|
||||||
|
return if (accessToken.isValid()) {
|
||||||
|
accessToken
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun evict() {
|
||||||
|
getCache()?.evict(hpkProperties.webServices.clientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <T> safeCast(item: Any, typeReference: TypeReference<T>): T? {
|
||||||
|
return try {
|
||||||
|
objectMapper.convertValue(item, typeReference)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logBuilderFactory.create(this::class.java)
|
||||||
|
.setMethod("safeCast")
|
||||||
|
.setError(e)
|
||||||
|
.build()
|
||||||
|
.error()
|
||||||
|
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun AccessToken.isValid(): Boolean {
|
||||||
|
val expirationTime = with(hpkProperties.webServices.oauth) {
|
||||||
|
Instant.now().plus(http.connectionTimeout + http.readTimeout + http.writeTimeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
return expiresAt.isAfter(expirationTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCache(): Cache? {
|
||||||
|
return cacheManager.getCache(HMS_CLIENT_SERVICE_CACHE_KEY)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
const val HMS_CLIENT_SERVICE_CACHE_KEY = "HMS_CLIENT_SERVICE"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package ru.touchin.push.message.provider.hpk.services
|
||||||
|
|
||||||
|
interface HmsOauthClientService {
|
||||||
|
|
||||||
|
fun getAccessToken(): String
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
package ru.touchin.push.message.provider.hpk.services
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import ru.touchin.push.message.provider.exceptions.PushMessageProviderException
|
||||||
|
import ru.touchin.push.message.provider.hpk.clients.hms_oauth.HmsOauthWebClient
|
||||||
|
import ru.touchin.push.message.provider.hpk.services.dto.AccessToken
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class HmsOauthClientServiceImpl(
|
||||||
|
private val hmsOauthAccessTokenCacheService: HmsOauthAccessTokenCacheService,
|
||||||
|
private val hmsOauthWebClient: HmsOauthWebClient,
|
||||||
|
) : HmsOauthClientService {
|
||||||
|
|
||||||
|
override fun getAccessToken(): String {
|
||||||
|
val accessToken = hmsOauthAccessTokenCacheService.get()
|
||||||
|
?: retrieveAccessToken().also(hmsOauthAccessTokenCacheService::put)
|
||||||
|
|
||||||
|
return accessToken.value
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun retrieveAccessToken(): AccessToken {
|
||||||
|
val result = hmsOauthWebClient.token()
|
||||||
|
|
||||||
|
if (result.success == null) {
|
||||||
|
throw PushMessageProviderException(result.failure?.errorDescription.orEmpty(), null)
|
||||||
|
}
|
||||||
|
|
||||||
|
return with(result.success) {
|
||||||
|
AccessToken(
|
||||||
|
value = accessToken,
|
||||||
|
expiresAt = Instant.now().plusSeconds(expiresIn)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
package ru.touchin.push.message.provider.hpk.services
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import ru.touchin.push.message.provider.dto.request.PushTokenCheck
|
||||||
|
import ru.touchin.push.message.provider.dto.request.PushTokenMessage
|
||||||
|
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.SendPushTokenMessageResult
|
||||||
|
import ru.touchin.push.message.provider.enums.PushMessageProviderType
|
||||||
|
import ru.touchin.push.message.provider.exceptions.InvalidPushTokenException
|
||||||
|
import ru.touchin.push.message.provider.exceptions.PushMessageProviderException
|
||||||
|
import ru.touchin.push.message.provider.services.PushMessageProviderService
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class PushMessageProviderHpkService(
|
||||||
|
private val hmsHpkClientService: HmsHpkClientService,
|
||||||
|
) : PushMessageProviderService {
|
||||||
|
|
||||||
|
override val type: PushMessageProviderType = PushMessageProviderType.HPK
|
||||||
|
|
||||||
|
@Throws(PushMessageProviderException::class, InvalidPushTokenException::class)
|
||||||
|
override fun send(request: SendPushRequest): SendPushResult {
|
||||||
|
when (request) {
|
||||||
|
is PushTokenMessage -> hmsHpkClientService.send(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
return SendPushTokenMessageResult
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun check(request: PushTokenCheck): CheckPushTokenResult {
|
||||||
|
val result = hmsHpkClientService.check(request)
|
||||||
|
|
||||||
|
return CheckPushTokenResult(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
package ru.touchin.push.message.provider.hpk.services.dto
|
||||||
|
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
data class AccessToken(
|
||||||
|
val value: String,
|
||||||
|
val expiresAt: Instant,
|
||||||
|
)
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue