Compare commits

..

5 Commits

Author SHA1 Message Date
Artyom ad61be705b Create FcmClient class to make integration implementation closed 2022-08-12 15:13:48 +03:00
Artyom 37fb9443ea Update PushMessageProviderServiceFactory implementation 2022-08-12 12:29:30 +03:00
Artyom 5f81afcb08 Update readme 2022-08-11 17:49:47 +03:00
Artyom 1fffef06d9 Add push-message-provider-fcm module 2022-08-11 17:46:56 +03:00
Artyom 47759afac8 Add push-message-provider module 2022-08-11 17:46:54 +03:00
177 changed files with 170 additions and 6368 deletions

239
README.md
View File

@ -19,19 +19,6 @@
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
@ -81,106 +68,6 @@ Reports are stored in "$pwd/build/reports/diktat-report.html".
Утилиты для тестирования репозиториев Утилиты для тестирования репозиториев
## 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
Основные компоненты логирования: Основные компоненты логирования:
@ -329,53 +216,13 @@ 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
Модуль по обеспечению интеграции с Firebase Cloud Messaging. Модуль по обеспечению интеграции с Firebase Cloud Messaging.
1) Подключение компонентов Spring осуществляется при помощи аннотации `@EnablePushMessageProviderFcm`. 1) Подключение компонентов Spring осуществляется при помощи аннотации `@EnablePushMessageProviderFcm`.
2) Необходимо добавление конфигурации для модуля с выбранным способом хранения данных для авторизации. Пример файла конфигурации в формате yaml: 2) Необходимо добавление конфигурации для модуля. Пример файла конфигурации в формате yaml:
``` yaml ``` yaml
push-message-provider: push-message-provider:
platformProviders: platformProviders:
@ -384,87 +231,11 @@ push-message-provider:
IOS: IOS:
- FCM - FCM
fcm: fcm:
appName: yourAppName appName: ${appName}
auth: auth:
# Выбранный тип авторизации resourcePath: credentials/firebase-admin.json
client: client:
readTimeout: 10s readTimeout: 10s
connectionTimeout: 1s connectionTimeout: 1s
``` ```
3) Настраивается способ предоставления авторизации для Firebase Cloud Messaging. 3) По обозначенному пути `push-message-provider-fcm.auth.resourcePath` добавляется json файл с настройками и доступами из консоли Firebase.
А) Токен доступа из консоли Google, добавляемый в конфигурацию настроек:
``` yaml
auth:
token:
value: testValue
expiresAt: 2023-01-01 23:59:59 +00:00
```
B) Данные в файле из консоли Firebase, добавляемые в resources с обозначением пути в конфигурации настроек:
``` yaml
auth:
credentialsFile:
path: credentials/firebase-admin.json
```
C) Данные из файла консоли Firebase, добавляемые в конфигурацию настроек:
``` yaml
auth:
credentialsData:
type: service_account
projectId: yourProjectId
privateKeyId: yourPrivateKeyId
privateKey: |
-----BEGIN PRIVATE KEY-----
-----END PRIVATE KEY-----
clientEmail: yourClientEmail
clientId: yourClientId
authUri: yourAuthUri
tokenUri: yourTokenUri
authProviderX509CertUrl: yourAuthProviderX509CertUrl
clientX509CertUrl: yourClientX509CertUrl
```
## push-message-provider-hpk
Модуль по обеспечению интеграции с Huawei Push Kit.
1) Подключение нового провайдера осуществляется при помощи аннотации `@EnablePushMessageProviderHpk`.
2) Для логирования запросов к сервису HPK нужно встроить в контейнер Spring собственный `WebClientLogger` из модуля `logger-spring-web` или же использовать стандартный посредством импорта конфигурации:
``` kotlin
@Import(
SpringLoggerConfiguration::class,
SpringLoggerWebConfiguration::class
)
class YourConfiguration
```
3) Нужно добавить конфигурацию для считывания модулем. Пример файла в формате yaml:
``` yaml
push-message-provider:
platformProviders:
ANDROID_HUAWEI:
- HPK
hpk:
web-services:
client-id: yourClientId
oauth:
client-secret: yourClientSecret
url: https://oauth-login.cloud.huawei.com/oauth2/v3/
http:
connection-timeout: 1s
read-timeout: 10s
write-timeout: 10s
ssl: # Опциональная структура
handshake-timeout: 1s
notify-read-timeout: 1s
notify-flush-timeout: 1s
hpk:
url: https://push-api.cloud.huawei.com/v1/
http:
connection-timeout: 1s
read-timeout: 10s
write-timeout: 10s
ssl: # Опциональная структура
handshake-timeout: 1s
notify-read-timeout: 1s
notify-flush-timeout: 1s
```

View File

@ -1,11 +1,9 @@
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
@ -17,9 +15,6 @@ 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 {
@ -35,30 +30,6 @@ 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")
@ -69,26 +40,6 @@ 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)
@ -99,15 +50,13 @@ 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.18.3") dependency("org.testcontainers:testcontainers:1.15.1")
dependency("org.testcontainers:postgresql:1.18.3") dependency("org.testcontainers:postgresql:1.15.1")
dependency("org.testcontainers:junit-jupiter:1.18.3") dependency("org.testcontainers:junit-jupiter:1.15.1")
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")
@ -125,15 +74,13 @@ 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")
} }

View File

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

View File

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

View File

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

View File

@ -1,7 +0,0 @@
plugins {
id("kotlin")
}
dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
}

View File

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

View File

@ -15,9 +15,5 @@ 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
}
} }

View File

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

View File

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

View File

@ -19,12 +19,12 @@ object StringUtils {
} }
nextUpperCase -> { nextUpperCase -> {
this.append(char.uppercase()) this.append(char.toUpperCase())
nextUpperCase = false nextUpperCase = false
} }
else -> { !nextUpperCase -> {
this.append(char.lowercase()) this.append(char.toLowerCase())
} }
} }
} }
@ -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()
} }

View File

@ -1,402 +0,0 @@
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.*'

View File

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

View File

@ -1,5 +1,4 @@
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

View File

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

View File

@ -1,6 +1,4 @@
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
detektGradlePluginVersion=1.18.0 kotlinVersion=1.5.21
diktatGradlePluginVersion=1.2.5
kotlinVersion=1.6.21

View File

@ -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-7.5.1-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

View File

@ -4,7 +4,6 @@ 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
@ -23,7 +22,6 @@ 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()
@ -48,11 +46,8 @@ class LogAspect(
.build() .build()
.error() .error()
} catch (logError: Throwable) { } catch (logError: Throwable) {
LoggerFactory.getLogger(this::class.java) error.printStackTrace()
.let { logger -> logError.printStackTrace()
logger.error("Cannot build logger", error)
logger.error("Cannot create logger", logError)
}
} }
} }

View File

@ -16,7 +16,6 @@ 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

View File

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

View File

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

View File

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

View File

@ -1,10 +1,5 @@
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
@ -31,14 +26,6 @@ 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()
@ -66,20 +53,9 @@ 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)
}
} }

View File

@ -1,20 +1,33 @@
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")
@ -26,7 +39,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 ->
""" """
{ {

View File

@ -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()

View File

@ -4,7 +4,6 @@ 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(",")}")
@ -12,18 +11,6 @@ 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

View File

@ -3,11 +3,9 @@ package ru.touchin.push.message.provider.fcm.clients
import com.google.firebase.messaging.FirebaseMessaging import com.google.firebase.messaging.FirebaseMessaging
import com.google.firebase.messaging.FirebaseMessagingException import com.google.firebase.messaging.FirebaseMessagingException
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
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.SendPushTokenMessageTraceableResult import ru.touchin.push.message.provider.dto.result.SendPushTokenMessageResult
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
import ru.touchin.push.message.provider.fcm.converters.FirebaseMessagingExceptionConverter import ru.touchin.push.message.provider.fcm.converters.FirebaseMessagingExceptionConverter
@ -26,35 +24,12 @@ class FcmClient(
@Throws(PushMessageProviderException::class, InvalidPushTokenException::class) @Throws(PushMessageProviderException::class, InvalidPushTokenException::class)
fun sendPushTokenMessage(request: PushTokenMessage): SendPushResult { fun sendPushTokenMessage(request: PushTokenMessage): SendPushResult {
return sendToPushToken(request, dryRun = false)
}
fun check(request: PushTokenCheck): PushTokenStatus {
val validationRequest = PushTokenMessage(
token = request.pushToken,
pushMessageNotification = null,
data = emptyMap()
)
return try {
sendToPushToken(validationRequest, dryRun = true)
PushTokenStatus.VALID
} catch (ipte: InvalidPushTokenException) {
PushTokenStatus.INVALID
} catch (pmpe: PushMessageProviderException) {
PushTokenStatus.UNKNOWN
}
}
@Throws(PushMessageProviderException::class, InvalidPushTokenException::class)
private fun sendToPushToken(request: PushTokenMessage, dryRun: Boolean): SendPushResult {
val message = pushTokenMessageConverter(request) val message = pushTokenMessageConverter(request)
return try { return try {
val messageId = firebaseMessaging.send(message, dryRun) val messageId = firebaseMessaging.send(message)
SendPushTokenMessageTraceableResult(messageId) SendPushTokenMessageResult(messageId)
} catch (e: FirebaseMessagingException) { } catch (e: FirebaseMessagingException) {
throw firebaseMessagingExceptionConverter(e) throw firebaseMessagingExceptionConverter(e)
} }

View File

@ -1,15 +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.SerializationFeature
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
import com.google.firebase.FirebaseOptions import com.google.firebase.FirebaseOptions
import com.google.firebase.messaging.FirebaseMessaging import com.google.firebase.messaging.FirebaseMessaging
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.boot.context.properties.ConfigurationPropertiesScan import org.springframework.boot.context.properties.ConfigurationPropertiesScan
import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.ComponentScan import org.springframework.context.annotation.ComponentScan
@ -17,8 +11,6 @@ import org.springframework.context.annotation.Import
import org.springframework.core.io.ClassPathResource import org.springframework.core.io.ClassPathResource
import ru.touchin.push.message.provider.configurations.PushMessageProviderConfiguration import ru.touchin.push.message.provider.configurations.PushMessageProviderConfiguration
import ru.touchin.push.message.provider.fcm.properties.PushMessageProviderFcmProperties import ru.touchin.push.message.provider.fcm.properties.PushMessageProviderFcmProperties
import java.text.SimpleDateFormat
import java.util.*
@ComponentScan("ru.touchin.push.message.provider.fcm") @ComponentScan("ru.touchin.push.message.provider.fcm")
@ConfigurationPropertiesScan(basePackages = ["ru.touchin.push.message.provider.fcm"]) @ConfigurationPropertiesScan(basePackages = ["ru.touchin.push.message.provider.fcm"])
@ -27,31 +19,9 @@ class PushMessageProviderFcmConfiguration {
@Bean @Bean
fun firebaseMessaging( fun firebaseMessaging(
properties: PushMessageProviderFcmProperties, properties: PushMessageProviderFcmProperties
@Qualifier("push-message-provider.fcm.credentials-object-mapper")
objectMapper: ObjectMapper
): FirebaseMessaging { ): FirebaseMessaging {
val credentials = when { val credentials = GoogleCredentials.fromStream(ClassPathResource(properties.auth.resourcePath).inputStream)
properties.auth.credentialsData != null -> {
GoogleCredentials.fromStream(
objectMapper.writeValueAsString(properties.auth.credentialsData).byteInputStream(Charsets.UTF_8)
)
}
properties.auth.credentialsFile != null -> {
GoogleCredentials.fromStream(
ClassPathResource(properties.auth.credentialsFile.path).inputStream
)
}
properties.auth.token != null -> {
GoogleCredentials.create(
AccessToken(properties.auth.token.value, properties.auth.token.expiresAt)
)
}
else -> throw IllegalStateException("No more authorization types allowed.")
}
val options: FirebaseOptions = FirebaseOptions.builder() val options: FirebaseOptions = FirebaseOptions.builder()
.setCredentials(credentials) .setCredentials(credentials)
@ -64,22 +34,4 @@ class PushMessageProviderFcmConfiguration {
return FirebaseMessaging.getInstance(firebaseApp) return FirebaseMessaging.getInstance(firebaseApp)
} }
@Bean("push-message-provider.fcm.credentials-date-format")
fun simpleDateFormat(): SimpleDateFormat {
return SimpleDateFormat("yyyy-MM-dd HH:mm:ss X", Locale.getDefault())
}
@Bean("push-message-provider.fcm.credentials-object-mapper")
fun objectMapper(
@Qualifier("push-message-provider.fcm.credentials-date-format")
simpleDateFormat: SimpleDateFormat
): ObjectMapper {
return ObjectMapper().apply {
configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false)
setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE)
setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY)
dateFormat = simpleDateFormat
}
}
} }

View File

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

View File

@ -13,17 +13,11 @@ 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, MessagingErrorCode.UNREGISTERED -> InvalidPushTokenException()
MessagingErrorCode.SENDER_ID_MISMATCH -> { else -> PushMessageProviderException(
InvalidPushTokenException() description = exception.message.orEmpty(),
} cause = exception
)
else -> {
PushMessageProviderException(
description = exception.message.orEmpty(),
cause = exception
)
}
} }
} }

View File

@ -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.Notification
@Component
class NotificationConverter {
operator fun invoke(notification: Notification): FcmNotification {
return FcmNotification.builder()
.setTitle(notification.title)
.setBody(notification.description)
.setImage(notification.imageUrl)
.build()
}
}

View File

@ -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.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()
}
}

View File

@ -1,67 +1,29 @@
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.PushMessageNotification import ru.touchin.push.message.provider.dto.Notification
import ru.touchin.push.message.provider.dto.request.PushTokenMessage import ru.touchin.push.message.provider.dto.request.PushTokenMessage
@Component("push-message-provider.fcm.push-token-message-converter") @Component
class PushTokenMessageConverter( class PushTokenMessageConverter(
private val pushMessageNotificationConverter: PushMessageNotificationConverter private val notificationConverter: NotificationConverter
) { ) {
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)
.setupApns() .setIfExists(request.notification)
.setupAndroid()
.setIfExists(request.pushMessageNotification)
.putAllData(request.data) .putAllData(request.data)
.build() .build()
} }
private fun Message.Builder.setIfExists(pushMessageNotification: PushMessageNotification?): Message.Builder { private fun Message.Builder.setIfExists(notification: Notification?): Message.Builder {
return if (pushMessageNotification != null) { return if (notification != null) {
setNotification(pushMessageNotificationConverter(pushMessageNotification)) setNotification(notificationConverter(notification))
} 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()
)
}
} }

View File

@ -1,64 +1,28 @@
package ru.touchin.push.message.provider.fcm.properties package ru.touchin.push.message.provider.fcm.properties
import com.fasterxml.jackson.databind.PropertyNamingStrategies
import com.fasterxml.jackson.databind.annotation.JsonNaming
import org.springframework.boot.context.properties.ConfigurationProperties import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.ConstructorBinding import org.springframework.boot.context.properties.ConstructorBinding
import org.springframework.util.Assert import ru.touchin.push.message.provider.enums.PlatformType
import ru.touchin.push.message.provider.enums.PushMessageProviderType
import ru.touchin.push.message.provider.properties.PushMessageProviderProperties
import java.time.Duration import java.time.Duration
import java.util.*
@ConstructorBinding @ConstructorBinding
@ConfigurationProperties(prefix = "push-message-provider.fcm") @ConfigurationProperties(prefix = "push-message-provider.fcm")
class PushMessageProviderFcmProperties( class PushMessageProviderFcmProperties(
val appName: String, val appName: String,
val auth: Auth, val auth: Auth.Credentials,
val client: Client, val client: Client
) { ) {
data class Auth( sealed interface Auth {
val credentialsFile: CredentialsFile?,
val credentialsData: CredentialsData?,
val token: AccessToken?,
) {
init { data class Credentials(
Assert.notNull( val resourcePath: String
arrayOf(credentialsFile, credentialsData, token).mapNotNull { it }.firstOrNull(), ) : Auth
"Authorization configuration is not provided."
)
Assert.notNull(
arrayOf(credentialsFile, credentialsData, token).mapNotNull { it }.singleOrNull(),
"Authorization must be configured using only single provider."
)
}
} }
data class CredentialsFile(
val path: String
)
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy::class)
data class CredentialsData(
val type: String,
val projectId: String,
val privateKeyId: String,
val privateKey: String,
val clientEmail: String,
val clientId: String,
val authUri: String,
val tokenUri: String,
val authProviderX509CertUrl: String,
val clientX509CertUrl: String
)
data class AccessToken(
val value: String,
val expiresAt: Date
)
data class Client( data class Client(
val readTimeout: Duration, val readTimeout: Duration,
val connectionTimeout: Duration val connectionTimeout: Duration

View File

@ -1,10 +1,8 @@
package ru.touchin.push.message.provider.fcm.services package ru.touchin.push.message.provider.fcm.services
import org.springframework.stereotype.Service 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.PushTokenMessage
import ru.touchin.push.message.provider.dto.request.SendPushRequest 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.SendPushResult
import ru.touchin.push.message.provider.enums.PushMessageProviderType import ru.touchin.push.message.provider.enums.PushMessageProviderType
import ru.touchin.push.message.provider.exceptions.InvalidPushTokenException import ru.touchin.push.message.provider.exceptions.InvalidPushTokenException
@ -26,10 +24,4 @@ class PushMessageProviderFcmService(
} }
} }
override fun check(request: PushTokenCheck): CheckPushTokenResult {
val status = fcmClient.check(request)
return CheckPushTokenResult(status)
}
} }

View File

@ -1,22 +1,29 @@
package ru.touchin.push.message.provider.fcm package ru.touchin.push.message.provider.fcm
import com.google.firebase.FirebaseApp import com.fasterxml.jackson.annotation.JsonAutoDetect
import com.fasterxml.jackson.annotation.PropertyAccessor
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.SerializationFeature
import org.springframework.boot.SpringBootConfiguration import org.springframework.boot.SpringBootConfiguration
import org.springframework.boot.context.properties.ConfigurationPropertiesScan
import org.springframework.boot.test.context.TestConfiguration import org.springframework.boot.test.context.TestConfiguration
import org.springframework.context.ApplicationListener import org.springframework.context.annotation.Bean
import org.springframework.context.event.ContextRefreshedEvent import org.springframework.context.annotation.Import
import org.springframework.test.context.ContextConfiguration
import ru.touchin.push.message.provider.fcm.configurations.PushMessageProviderFcmConfiguration
@TestConfiguration @TestConfiguration
@SpringBootConfiguration @SpringBootConfiguration
@EnablePushMessageProviderFcm @EnablePushMessageProviderFcm
class PushMessageProviderFcmTestApplication : ApplicationListener<ContextRefreshedEvent> { class PushMessageProviderFcmTestApplication {
override fun onApplicationEvent(event: ContextRefreshedEvent) { @Bean
clearSingletonsOutsideContainer() fun objectMapper(): ObjectMapper {
} return ObjectMapper().apply {
configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false)
private fun clearSingletonsOutsideContainer() { setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE)
FirebaseApp.getApps().forEach(FirebaseApp::delete) setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY)
}
} }
} }

View File

@ -1,54 +0,0 @@
package ru.touchin.push.message.provider.fcm.configurations
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import ru.touchin.push.message.provider.fcm.properties.PushMessageProviderFcmProperties
import java.lang.IllegalArgumentException
import java.util.*
class PushMessageProviderFcmPropertiesAuthTest {
@Test
@DisplayName("Один тип авторизации может быть выбран")
fun constructor_singleConfigurationShouldBeOk() {
PushMessageProviderFcmProperties.Auth(
credentialsFile = null,
credentialsData = null,
token = PushMessageProviderFcmProperties.AccessToken(
value = "testToken",
expiresAt = Date()
)
)
}
@Test
@DisplayName("При отсутствии типов авторизации выбрасывается исключение")
fun constructor_configurationMustExist() {
Assertions.assertThrows(IllegalArgumentException::class.java) {
PushMessageProviderFcmProperties.Auth(
credentialsFile = null,
credentialsData = null,
token = null
)
}
}
@Test
@DisplayName("При нескольких типах авторизации выбрасывается исключение")
fun constructor_configurationMustBeSingle() {
Assertions.assertThrows(IllegalArgumentException::class.java) {
PushMessageProviderFcmProperties.Auth(
credentialsFile = PushMessageProviderFcmProperties.CredentialsFile(
path = "testPath"
),
credentialsData = null,
token = PushMessageProviderFcmProperties.AccessToken(
value = "testToken",
expiresAt = Date()
)
)
}
}
}

View File

@ -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.PushMessageNotification import ru.touchin.push.message.provider.dto.Notification
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 PushMessageNotificationConverterTest { class NotificationConverterTest {
@Autowired @Autowired
lateinit var pushMessageNotificationConverter: PushMessageNotificationConverter lateinit var notificationConverter: NotificationConverter
@Autowired @Autowired
lateinit var objectMapper: ObjectMapper lateinit var objectMapper: ObjectMapper
@ -21,19 +21,19 @@ class PushMessageNotificationConverterTest {
@Test @Test
@DisplayName("Конвертация уведомления происходит корректно") @DisplayName("Конвертация уведомления происходит корректно")
fun invoke_basic() { fun invoke_basic() {
val pushMessageNotification = PushMessageNotification( val notification = Notification(
title = "title", title = "title",
description = "description", description = "description",
imageUrl = "imageUrl" imageUrl = "imageUrl"
) )
val realResult = pushMessageNotificationConverter(pushMessageNotification) val realResult = notificationConverter(notification)
val realResultJson = objectMapper.writeValueAsString(realResult) val realResultJson = objectMapper.writeValueAsString(realResult)
val expectedResult = FcmNotification.builder() val expectedResult = FcmNotification.builder()
.setTitle(pushMessageNotification.title) .setTitle(notification.title)
.setBody(pushMessageNotification.description) .setBody(notification.description)
.setImage(pushMessageNotification.imageUrl) .setImage(notification.imageUrl)
.build() .build()
val expectedResultJson = objectMapper.writeValueAsString(expectedResult) val expectedResultJson = objectMapper.writeValueAsString(expectedResult)

View File

@ -1,17 +1,13 @@
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.PushMessageNotification import ru.touchin.push.message.provider.dto.Notification
import ru.touchin.push.message.provider.dto.request.PushTokenMessage import ru.touchin.push.message.provider.dto.request.PushTokenMessage
@SpringBootTest @SpringBootTest
@ -21,7 +17,7 @@ class PushTokenMessageConverterTest {
lateinit var pushTokenMessageConverter: PushTokenMessageConverter lateinit var pushTokenMessageConverter: PushTokenMessageConverter
@Autowired @Autowired
lateinit var pushMessageNotificationConverter: PushMessageNotificationConverter lateinit var notificationConverter: NotificationConverter
@Autowired @Autowired
lateinit var objectMapper: ObjectMapper lateinit var objectMapper: ObjectMapper
@ -29,14 +25,14 @@ class PushTokenMessageConverterTest {
@Test @Test
@DisplayName("Конвертация сообщения с уведомлением происходит корректно") @DisplayName("Конвертация сообщения с уведомлением происходит корректно")
fun invoke_withNotification() { fun invoke_withNotification() {
val pushMessageNotification = PushMessageNotification( val notification = Notification(
title = "title", title = "title",
description = "description", description = "description",
imageUrl = "imageUrl" imageUrl = "imageUrl"
) )
val pushTokenMessage = PushTokenMessage( val pushTokenMessage = PushTokenMessage(
token = "token", token = "token",
pushMessageNotification = pushMessageNotification, notification = notification,
data = mapOf("testKey" to "testvalue") data = mapOf("testKey" to "testvalue")
) )
@ -45,10 +41,8 @@ class PushTokenMessageConverterTest {
val expectedResult = Message.builder() val expectedResult = Message.builder()
.setToken(pushTokenMessage.token) .setToken(pushTokenMessage.token)
.setNotification(pushMessageNotificationConverter(pushMessageNotification)) .setNotification(notificationConverter(notification))
.putAllData(pushTokenMessage.data) .putAllData(pushTokenMessage.data)
.setupApns()
.setupAndroid()
.build() .build()
val expectedResultJson = objectMapper.writeValueAsString(expectedResult) val expectedResultJson = objectMapper.writeValueAsString(expectedResult)
@ -65,7 +59,7 @@ class PushTokenMessageConverterTest {
fun invoke_withoutNotification() { fun invoke_withoutNotification() {
val pushTokenMessage = PushTokenMessage( val pushTokenMessage = PushTokenMessage(
token = "token", token = "token",
pushMessageNotification = null, notification = null,
data = mapOf("testKey" to "testvalue") data = mapOf("testKey" to "testvalue")
) )
@ -75,8 +69,6 @@ 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)
@ -88,29 +80,4 @@ 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()
)
}
} }

View File

@ -7,11 +7,8 @@ import org.mockito.Mockito
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 org.springframework.boot.test.mock.mockito.MockBean 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.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.SendPushTokenMessageResult
import ru.touchin.push.message.provider.dto.result.SendPushTokenMessageTraceableResult
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 +26,11 @@ class PushMessageProviderFcmServiceTest {
fun send_basic() { fun send_basic() {
val request = PushTokenMessage( val request = PushTokenMessage(
token = "testToken", token = "testToken",
pushMessageNotification = null, notification = null,
data = emptyMap() data = emptyMap()
) )
val expectedResult = SendPushTokenMessageTraceableResult("testMessageId") val expectedResult = SendPushTokenMessageResult("testMessageId")
Mockito.`when`( Mockito.`when`(
fcmClient.sendPushTokenMessage(request) fcmClient.sendPushTokenMessage(request)
@ -50,25 +47,4 @@ class PushMessageProviderFcmServiceTest {
) )
} }
@Test
@DisplayName("Обработка запроса на валидацию пуш-токена происходит корректно")
fun isValid_basic() {
val expectedClientResult = PushTokenStatus.VALID
Mockito.`when`(
fcmClient.check(PushTokenCheck("testToken"))
).thenReturn(
expectedClientResult
)
val expectedResult = CheckPushTokenResult(expectedClientResult)
val realResult = pushMessageProviderService.check(PushTokenCheck("testToken"))
Assert.assertEquals(
"Обработка запроса на валидацию пуш-токена происходит корректно",
expectedResult,
realResult
)
}
} }

View File

@ -7,9 +7,7 @@ push-message-provider:
fcm: fcm:
appName: testAppName appName: testAppName
auth: auth:
token: resourcePath: credentials/firebase-admin.json
value: testValue
expiresAt: 2023-01-01 23:59:59 +00:00
client: client:
readTimeout: 10s readTimeout: 10s
connectionTimeout: 1s connectionTimeout: 1s

View File

@ -0,0 +1,12 @@
{
"type": "service_account",
"project_id": "testProjectId",
"private_key_id": "privateKeyId",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBALfBshaLMW2yddmAZJRNXTZzcSbwvY93Dnjj6naWgoBJoB3mOM5bcoyWwBw12A4rwecorz74OUOc6zdqX3j8hwsSyzgAUStKM5PkOvPNRKsI4eXAWU0fmb8h1jyXwftl7EzeBjEMBTpyXkgDk3wLfHN6ciCZrnQndOvS+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=\n-----END PRIVATE KEY-----\n",
"client_email": "clientEmail",
"client_id": "clientId",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://accounts.google.com/o/oauth2/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "clientX509CertUrl"
}

View File

@ -1,21 +0,0 @@
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")
}

View File

@ -1,7 +0,0 @@
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

View File

@ -1,3 +0,0 @@
package ru.touchin.push.message.provider.hpk.base.builders
internal interface Buildable

View File

@ -1,42 +0,0 @@
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
}
}
}

View File

@ -1,85 +0,0 @@
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,
)
}
}
}

View File

@ -1,18 +0,0 @@
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
}

View File

@ -1,12 +0,0 @@
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
}

View File

@ -1,7 +0,0 @@
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
}

View File

@ -1,54 +0,0 @@
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
}
}
}

View File

@ -1,71 +0,0 @@
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"
}
}

View File

@ -1,12 +0,0 @@
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,
)

View File

@ -1,165 +0,0 @@
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()
}
}

View File

@ -1,76 +0,0 @@
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()
}
}

View File

@ -1,136 +0,0 @@
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()
}
}

View File

@ -1,72 +0,0 @@
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()
}
}

View File

@ -1,77 +0,0 @@
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()
}
}

View File

@ -1,78 +0,0 @@
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()
}
}

View File

@ -1,80 +0,0 @@
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()
}
}

View File

@ -1,93 +0,0 @@
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()
}
}

View File

@ -1,82 +0,0 @@
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()
}
}

View File

@ -1,63 +0,0 @@
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()
}
}

View File

@ -1,423 +0,0 @@
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()
}
}

View File

@ -1,107 +0,0 @@
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()
}
}

View File

@ -1,109 +0,0 @@
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()
}
}

View File

@ -1,38 +0,0 @@
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()
}
}

View File

@ -1,89 +0,0 @@
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()
}
}

View File

@ -1,58 +0,0 @@
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()
}
}

View File

@ -1,52 +0,0 @@
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()
}
}

View File

@ -1,151 +0,0 @@
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()
}
}

View File

@ -1,70 +0,0 @@
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()
}
}

View File

@ -1,17 +0,0 @@
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),
}

View File

@ -1,13 +0,0 @@
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),
}

View File

@ -1,12 +0,0 @@
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),
}

View File

@ -1,13 +0,0 @@
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
}

View File

@ -1,12 +0,0 @@
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),
}

View File

@ -1,13 +0,0 @@
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),
}

View File

@ -1,13 +0,0 @@
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),
}

View File

@ -1,13 +0,0 @@
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"),
}

View File

@ -1,12 +0,0 @@
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"),
}

View File

@ -1,24 +0,0 @@
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"),
}

View File

@ -1,12 +0,0 @@
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),
}

View File

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

View File

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

View File

@ -1,10 +0,0 @@
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,
)

View File

@ -1,5 +0,0 @@
package ru.touchin.push.message.provider.hpk.clients.hms_hpk.requests
internal open class HmsHpkRequest(
val accessToken: String,
)

View File

@ -1,14 +0,0 @@
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,
)

View File

@ -1,78 +0,0 @@
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"
}
}

View File

@ -1,7 +0,0 @@
package ru.touchin.push.message.provider.hpk.clients.hms_oauth.response
internal class HmsOauthErrorResponse(
val error: Int,
val subError: Int,
val errorDescription: String,
)

View File

@ -1,8 +0,0 @@
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,
)

View File

@ -1,62 +0,0 @@
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()
}

View File

@ -1,19 +0,0 @@
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()
}
}

View File

@ -1,56 +0,0 @@
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
}
}

View File

@ -1,59 +0,0 @@
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,
)
}

View File

@ -1,13 +0,0 @@
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
}

View File

@ -1,74 +0,0 @@
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)
}
}
}
}

View File

@ -1,11 +0,0 @@
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()
}

View File

@ -1,81 +0,0 @@
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"
}
}

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