Compare commits
6 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
5e0fce2a60 | |
|
|
a070a94381 | |
|
|
0976db1e47 | |
|
|
e17454b996 | |
|
|
c8ed908656 | |
|
|
20f07a4a9d |
113
README.md
113
README.md
|
|
@ -19,6 +19,19 @@
|
||||||
implementation("ru.touchin:common")
|
implementation("ru.touchin:common")
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Project Gradle tasks
|
||||||
|
### Detekt:
|
||||||
|
- `gradle $project:detekt` - detect not formatted, complex code.
|
||||||
|
|
||||||
|
Reports are stored in "$pwd/build/reports/kotlin-detekt-${project.name}.html".
|
||||||
|
|
||||||
|
### DiKTat:
|
||||||
|
- `gradle :diktatCheck` - detect not formatted code of "kt", "kts" files;
|
||||||
|
- `gradle :diktatFix` - if possible, fix not formatted code. Known issues: full fix may require 1+ launch in order to apply all rules; some rules potentially may break code syntax.
|
||||||
|
|
||||||
|
By setting environment variable `TASKS_FILE_REPORT_ENABLED`(true, false) you may configure raw console output or html file as report.
|
||||||
|
Reports are stored in "$pwd/build/reports/diktat-report.html".
|
||||||
|
|
||||||
## common
|
## common
|
||||||
|
|
||||||
|
|
@ -68,6 +81,106 @@
|
||||||
|
|
||||||
Утилиты для тестирования репозиториев
|
Утилиты для тестирования репозиториев
|
||||||
|
|
||||||
|
## codestyle-archunit
|
||||||
|
|
||||||
|
Набор правил для поддержки оформления архитектуры.
|
||||||
|
|
||||||
|
#### Список доступных правил
|
||||||
|
- `ru.touchin.codestyle.archunit.rules.ClassNamingArchRules`
|
||||||
|
- `ru.touchin.codestyle.archunit.rules.ClassPackagingArchRules`
|
||||||
|
|
||||||
|
|
||||||
|
#### Gradle plugin
|
||||||
|
Настройка и применение совместно с [ArchUnit Gradle Plugin](https://github.com/societe-generale/arch-unit-gradle-plugin).
|
||||||
|
|
||||||
|
Действие `checkRules` для проверки соответствие правилам запускается при операциях сборки, по умолчанию.
|
||||||
|
Вручную можно вызвать командой ``gradle :checkRules`` для нужного модуля.
|
||||||
|
Добавить его можно следующим образом на примере установки в рутовый gradle.build проекта:
|
||||||
|
|
||||||
|
Groovy DSL:
|
||||||
|
```groovy
|
||||||
|
buildscript {
|
||||||
|
dependencies {
|
||||||
|
classpath "com.societegenerale.commons:arch-unit-gradle-plugin:3.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
subprojects {
|
||||||
|
dependencyManagement {
|
||||||
|
dependencies {
|
||||||
|
dependency "com.tngtech.archunit:archunit:1.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply plugin: "java"
|
||||||
|
apply plugin: "com.societegenerale.commons.arch-unit-gradle-plugin"
|
||||||
|
|
||||||
|
archUnit {
|
||||||
|
mainScopePath = "/classes/kotlin/main" // or "/classes/java/main"
|
||||||
|
testScopePath = "/classes/kotlin/test" // or "/classes/java/test"
|
||||||
|
|
||||||
|
var applyType = applyOn("ru.touchin", "main")
|
||||||
|
configurableRules = [
|
||||||
|
configurableRule(
|
||||||
|
"ru.touchin.codestyle.archunit.rules.ClassNamingArchRules",
|
||||||
|
applyType,
|
||||||
|
),
|
||||||
|
configurableRule(
|
||||||
|
"ru.touchin.codestyle.archunit.rules.ClassPackagingArchRules",
|
||||||
|
applyType,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
archUnitExtraLib "ru.touchin:codestyle-archunit" // or archUnitExtraLib project(":codestyle-archunit")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Kotlin DSL:
|
||||||
|
```kotlin
|
||||||
|
plugins {
|
||||||
|
id("com.societegenerale.commons.arch-unit-gradle-plugin") version "3.0.0"
|
||||||
|
}
|
||||||
|
subprojects {
|
||||||
|
configure<DependencyManagementExtension> {
|
||||||
|
dependencies {
|
||||||
|
dependency("com.tngtech.archunit:archunit:1.0.1")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(plugin = "java")
|
||||||
|
apply(plugin = "com.societegenerale.commons.arch-unit-gradle-plugin")
|
||||||
|
|
||||||
|
archUnit {
|
||||||
|
mainScopePath = "/classes/kotlin/main" // or "/classes/java/main"
|
||||||
|
testScopePath = "/classes/kotlin/test" // or "/classes/java/test"
|
||||||
|
|
||||||
|
configurableRules = listOf(
|
||||||
|
"ru.touchin.codestyle.archunit.rules.ClassNamingArchRules",
|
||||||
|
"ru.touchin.codestyle.archunit.rules.ClassPackagingArchRules"
|
||||||
|
).map { package ->
|
||||||
|
configurableRule(
|
||||||
|
package,
|
||||||
|
applyOn("ru.touchin", "main")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
archUnitExtraLib("ru.touchin:codestyle-archunit") // or archUnitExtraLib(project(":codestyle-archunit"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Отключить проверки на таске помимо конфигурирования `configurableRule` можно также таким образом:
|
||||||
|
```kotlin
|
||||||
|
// clear action launch for root project to avoid exception
|
||||||
|
tasks.checkRules.configure {
|
||||||
|
actions.clear()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## logger
|
## logger
|
||||||
|
|
||||||
Основные компоненты логирования:
|
Основные компоненты логирования:
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||||
import io.spring.gradle.dependencymanagement.dsl.DependencyManagementExtension
|
import io.spring.gradle.dependencymanagement.dsl.DependencyManagementExtension
|
||||||
import io.gitlab.arturbosch.detekt.extensions.DetektExtension
|
import 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,7 +18,8 @@ plugins {
|
||||||
// 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").version("1.18.0")
|
id("io.gitlab.arturbosch.detekt")
|
||||||
|
id("org.cqfn.diktat.diktat-gradle-plugin")
|
||||||
}
|
}
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
|
|
@ -29,13 +31,45 @@ allprojects {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
println("Enabling IDEA plugin in project ${project.name}...")
|
||||||
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}...")
|
||||||
apply(plugin = "org.jetbrains.kotlin.jvm")
|
apply(plugin = "org.jetbrains.kotlin.jvm")
|
||||||
|
|
||||||
|
println("Enabling Kotlin Spring plugin in project ${project.name}...")
|
||||||
apply(plugin = "org.jetbrains.kotlin.plugin.spring")
|
apply(plugin = "org.jetbrains.kotlin.plugin.spring")
|
||||||
|
|
||||||
|
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")
|
apply(plugin = "io.gitlab.arturbosch.detekt")
|
||||||
|
|
||||||
detekt {
|
detekt {
|
||||||
|
|
@ -47,7 +81,7 @@ subprojects {
|
||||||
reports {
|
reports {
|
||||||
txt.enabled = false
|
txt.enabled = false
|
||||||
xml.enabled = false
|
xml.enabled = false
|
||||||
html{
|
html {
|
||||||
enabled = true
|
enabled = true
|
||||||
destination = file("${project.buildDir}/reports/kotlin-detekt-${project.name}.html")
|
destination = file("${project.buildDir}/reports/kotlin-detekt-${project.name}.html")
|
||||||
}
|
}
|
||||||
|
|
@ -65,15 +99,16 @@ subprojects {
|
||||||
dependency("ch.qos.logback.contrib:logback-json-classic:0.1.5")
|
dependency("ch.qos.logback.contrib:logback-json-classic:0.1.5")
|
||||||
dependency("ch.qos.logback.contrib:logback-jackson:0.1.5")
|
dependency("ch.qos.logback.contrib:logback-jackson:0.1.5")
|
||||||
|
|
||||||
dependency("org.testcontainers:testcontainers:1.15.1")
|
dependency("org.testcontainers:testcontainers:1.18.3")
|
||||||
dependency("org.testcontainers:postgresql:1.15.1")
|
dependency("org.testcontainers:postgresql:1.18.3")
|
||||||
dependency("org.testcontainers:junit-jupiter:1.15.1")
|
dependency("org.testcontainers:junit-jupiter:1.18.3")
|
||||||
dependency("org.junit.jupiter:junit-jupiter-api:5.4.2")
|
dependency("org.junit.jupiter:junit-jupiter-api:5.4.2")
|
||||||
dependency("org.junit.jupiter:junit-jupiter-params:5.4.2")
|
dependency("org.junit.jupiter:junit-jupiter-params:5.4.2")
|
||||||
dependency("org.junit.jupiter:junit-jupiter-engine:5.4.2")
|
dependency("org.junit.jupiter:junit-jupiter-engine:5.4.2")
|
||||||
|
|
||||||
|
dependency("com.tngtech.archunit:archunit:1.0.1")
|
||||||
|
|
||||||
dependency("org.liquibase:liquibase-core:4.4.0")
|
dependency("org.liquibase:liquibase-core:4.4.0")
|
||||||
dependency("org.telegram:telegrambots-spring-boot-starter:6.4.0")
|
|
||||||
|
|
||||||
dependency("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0")
|
dependency("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0")
|
||||||
dependency("org.mockito:mockito-inline:3.11.0")
|
dependency("org.mockito:mockito-inline:3.11.0")
|
||||||
|
|
@ -90,9 +125,6 @@ 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.google.api-client:google-api-client:1.33.0")
|
|
||||||
dependency("com.google.apis:google-api-services-sheets:v4-rev20210629-1.32.1")
|
|
||||||
dependency("com.google.auth:google-auth-library-oauth2-http:1.3.0")
|
|
||||||
|
|
||||||
dependency("com.github.ua-parser:uap-java:1.5.3")
|
dependency("com.github.ua-parser:uap-java:1.5.3")
|
||||||
}
|
}
|
||||||
|
|
@ -101,7 +133,7 @@ subprojects {
|
||||||
dependencies {
|
dependencies {
|
||||||
// use for @ConstructorBinding
|
// use for @ConstructorBinding
|
||||||
implementation("org.jetbrains.kotlin:kotlin-reflect")
|
implementation("org.jetbrains.kotlin:kotlin-reflect")
|
||||||
implementation ("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
|
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
|
||||||
|
|
||||||
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
|
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
plugins {
|
||||||
|
id("kotlin")
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(project(":common"))
|
||||||
|
implementation("com.tngtech.archunit:archunit")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
package ru.touchin.codestyle.archunit.rules
|
||||||
|
|
||||||
|
import com.tngtech.archunit.lang.ArchRule
|
||||||
|
import com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes
|
||||||
|
import com.tngtech.archunit.lang.syntax.ArchRuleDefinition.methods
|
||||||
|
import ru.touchin.common.exceptions.CommonException
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set of rules that defines constraint for classes naming.
|
||||||
|
*/
|
||||||
|
@Suppress("unused")
|
||||||
|
object ClassNamingArchRules {
|
||||||
|
|
||||||
|
|
||||||
|
val CLASSES_WHICH_HAVE_SCHEDULED_METHODS_MUST_HAVE_SUFFIX: ArchRule = methods()
|
||||||
|
.that()
|
||||||
|
.areAnnotatedWith("org.springframework.scheduling.annotation.Scheduled")
|
||||||
|
.should().beDeclaredInClassesThat().haveSimpleNameEndingWith("Job")
|
||||||
|
.allowEmptyShould(true)
|
||||||
|
.because("Classes that use 'Scheduled' annotation must have name with 'Job' suffix")
|
||||||
|
|
||||||
|
val ANNOTATED_SERVICE_CLASSES_MUST_HAVE_SUFFIX: ArchRule = methods()
|
||||||
|
.that().areAnnotatedWith("org.springframework.stereotype.Service")
|
||||||
|
.should().beDeclaredInClassesThat().haveSimpleNameContaining("Service")
|
||||||
|
.allowEmptyShould(true)
|
||||||
|
.because("Classes that use 'Service' annotation are assignable to class with `Service` suffix")
|
||||||
|
|
||||||
|
val ANNOTATED_ENTITY_CLASSES_MUST_HAVE_SUFFIX: ArchRule = classes()
|
||||||
|
.that().areAnnotatedWith("javax.persistence.Entity")
|
||||||
|
.should().haveSimpleNameEndingWith("Entity")
|
||||||
|
.allowEmptyShould(true)
|
||||||
|
.because("Hibernate 'Entity' classes must have name with 'Entity' suffix")
|
||||||
|
|
||||||
|
val EXCEPTION_CLASSES_MUST_HAVE_SUFFIX: ArchRule = classes()
|
||||||
|
.that().areAssignableTo(Exception::class.java)
|
||||||
|
.or().areAssignableTo(CommonException::class.java)
|
||||||
|
.should().haveSimpleNameEndingWith("Exception")
|
||||||
|
.allowEmptyShould(true)
|
||||||
|
.because("'Exception' classes must have name with 'Exception' suffix")
|
||||||
|
|
||||||
|
val JPAREPOSITORY_CLASSES_MUST_HAVE_SUFFIX: ArchRule = classes()
|
||||||
|
.that().areAssignableTo("org.springframework.data.jpa.repository.JpaRepository")
|
||||||
|
.should().haveSimpleNameEndingWith("Repository")
|
||||||
|
.allowEmptyShould(true)
|
||||||
|
.because("Classes that use 'JpaRepository' must have name with 'Repository' suffix")
|
||||||
|
|
||||||
|
val WEBCLIENT_CLASSES_MUST_HAVE_SUFFIX: ArchRule = classes()
|
||||||
|
.that().haveFullyQualifiedName("org.springframework.web.reactive.valction.client.WebClient")
|
||||||
|
.should().onlyBeAccessed().byClassesThat().haveSimpleNameEndingWith("WebClient")
|
||||||
|
.allowEmptyShould(true)
|
||||||
|
.because("Classes that use Spring 'WebClient' must have name with 'WebClient' suffix")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
package ru.touchin.codestyle.archunit.rules
|
||||||
|
|
||||||
|
import com.tngtech.archunit.lang.ArchRule
|
||||||
|
import com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set of rules that defines constraint for classes placement.
|
||||||
|
*/
|
||||||
|
@Suppress("unused")
|
||||||
|
object ClassPackagingArchRules {
|
||||||
|
|
||||||
|
|
||||||
|
val IMPL_CLASSES_ARE_IN_CORRESPONDING_PACKAGE: ArchRule = classes()
|
||||||
|
.that().haveSimpleNameEndingWith("Impl")
|
||||||
|
.should().resideInAPackage("..impl..")
|
||||||
|
.allowEmptyShould(true)
|
||||||
|
.because("Implementations of interfaces must reside in package 'impl'")
|
||||||
|
|
||||||
|
|
||||||
|
val ENTITIES_ARE_IN_CORRESPONDING_PACKAGE: ArchRule = classes()
|
||||||
|
.that().haveSimpleNameEndingWith("Entity")
|
||||||
|
.should().resideInAPackage("..models..")
|
||||||
|
.allowEmptyShould(true)
|
||||||
|
.because("'Entity' must reside in package 'models'")
|
||||||
|
|
||||||
|
val REPOSITORIES_ARE_IN_CORRESPONDING_PACKAGE: ArchRule = classes()
|
||||||
|
.that().haveSimpleNameEndingWith("Repository")
|
||||||
|
.should().resideInAPackage("..repositories..")
|
||||||
|
.allowEmptyShould(true)
|
||||||
|
.because("'Repository' must reside in package 'repositories'")
|
||||||
|
|
||||||
|
val CORE_SERVICES_ARE_IN_CORRESPONDING_PACKAGE: ArchRule = classes()
|
||||||
|
.that().haveSimpleNameEndingWith("CoreService")
|
||||||
|
.should().resideInAPackage("..services..")
|
||||||
|
.allowEmptyShould(true)
|
||||||
|
.because("'CoreService' must reside in package 'services'")
|
||||||
|
|
||||||
|
val CONVERTERS_ARE_IN_CORRESPONDING_PACKAGE: ArchRule = classes()
|
||||||
|
.that().haveSimpleNameEndingWith("Converter")
|
||||||
|
.should().resideInAPackage("..converters..")
|
||||||
|
.allowEmptyShould(true)
|
||||||
|
.because("'Converter' must reside in package 'converters'")
|
||||||
|
|
||||||
|
val EXCEPTIONS_ARE_IN_CORRESPONDING_PACKAGE: ArchRule = classes()
|
||||||
|
.that().haveSimpleNameEndingWith("Exception")
|
||||||
|
.should().resideInAPackage("..exceptions..")
|
||||||
|
.allowEmptyShould(true)
|
||||||
|
.because("'Exception' must reside in package 'exceptions'")
|
||||||
|
|
||||||
|
val ENUM_CLASSES_ARE_IN_CORRESPONDING_PACKAGE: ArchRule = classes()
|
||||||
|
.that().areEnums()
|
||||||
|
.should().resideInAPackage("..enums..")
|
||||||
|
.allowEmptyShould(true)
|
||||||
|
.because("'Enum' must reside in package 'enums'")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -6,7 +6,6 @@ import org.springframework.data.annotation.LastModifiedDate
|
||||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener
|
import org.springframework.data.jpa.domain.support.AuditingEntityListener
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
import java.time.ZonedDateTime
|
import java.time.ZonedDateTime
|
||||||
import javax.persistence.Column
|
|
||||||
import javax.persistence.EntityListeners
|
import javax.persistence.EntityListeners
|
||||||
import javax.persistence.MappedSuperclass
|
import javax.persistence.MappedSuperclass
|
||||||
|
|
||||||
|
|
@ -14,8 +13,7 @@ import javax.persistence.MappedSuperclass
|
||||||
@MappedSuperclass
|
@MappedSuperclass
|
||||||
abstract class BaseEntity : Serializable {
|
abstract class BaseEntity : Serializable {
|
||||||
|
|
||||||
@CreatedDate
|
@CreatedDate(updatable = false)
|
||||||
@Column(updatable = false)
|
|
||||||
lateinit var createdAt: ZonedDateTime
|
lateinit var createdAt: ZonedDateTime
|
||||||
|
|
||||||
@LastModifiedDate
|
@LastModifiedDate
|
||||||
|
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
package ru.touchin.common.spring.security.utils
|
|
||||||
|
|
||||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
|
|
||||||
|
|
||||||
object PasswordEncoder {
|
|
||||||
|
|
||||||
fun String.encode(): String = BCryptPasswordEncoder().encode(this)
|
|
||||||
|
|
||||||
fun String.verify(passwordHash: String) = BCryptPasswordEncoder().matches(this, passwordHash)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -4,18 +4,10 @@ package ru.touchin.common.spring.test.jpa.repository
|
||||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
|
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
|
||||||
import org.springframework.context.annotation.Import
|
import org.springframework.context.annotation.Import
|
||||||
import org.springframework.test.context.ActiveProfiles
|
import org.springframework.test.context.ActiveProfiles
|
||||||
import org.springframework.test.context.junit.jupiter.DisabledIf
|
|
||||||
import ru.touchin.common.spring.test.annotations.SlowTest
|
import ru.touchin.common.spring.test.annotations.SlowTest
|
||||||
|
|
||||||
@ActiveProfiles("test", "test-slow")
|
@ActiveProfiles("test", "test-slow")
|
||||||
@SlowTest
|
@SlowTest
|
||||||
@DataJpaTest
|
@DataJpaTest
|
||||||
@Import(RepositoryTestConfiguration::class)
|
@Import(RepositoryTestConfiguration::class)
|
||||||
@DisabledIf(
|
|
||||||
expression = """
|
|
||||||
#{systemProperties['tests.slow.enabled'] != null
|
|
||||||
? systemProperties['tests.slow.enabled'].toLowerCase().contains('false')
|
|
||||||
: false}
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
annotation class RepositoryTest
|
annotation class RepositoryTest
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ package ru.touchin.common.spring.test.jpa.repository
|
||||||
import com.zaxxer.hikari.HikariConfig
|
import com.zaxxer.hikari.HikariConfig
|
||||||
import com.zaxxer.hikari.HikariDataSource
|
import com.zaxxer.hikari.HikariDataSource
|
||||||
import org.springframework.beans.factory.annotation.Value
|
import org.springframework.beans.factory.annotation.Value
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
|
|
||||||
import org.springframework.boot.test.context.TestConfiguration
|
import org.springframework.boot.test.context.TestConfiguration
|
||||||
import org.springframework.context.annotation.Bean
|
import org.springframework.context.annotation.Bean
|
||||||
import org.springframework.context.annotation.ComponentScan
|
import org.springframework.context.annotation.ComponentScan
|
||||||
|
|
@ -19,7 +18,6 @@ import javax.sql.DataSource
|
||||||
@TestConfiguration
|
@TestConfiguration
|
||||||
@EnableJpaAuditingExtra
|
@EnableJpaAuditingExtra
|
||||||
@ComponentScan
|
@ComponentScan
|
||||||
@ConditionalOnProperty(name = ["tests.slow.enabled"], matchIfMissing = true)
|
|
||||||
class RepositoryTestConfiguration {
|
class RepositoryTestConfiguration {
|
||||||
|
|
||||||
// запуск и остановка контейнера по lifecycle-событиями компонента (1)
|
// запуск и остановка контейнера по lifecycle-событиями компонента (1)
|
||||||
|
|
|
||||||
|
|
@ -15,4 +15,4 @@ spring:
|
||||||
tests:
|
tests:
|
||||||
slow:
|
slow:
|
||||||
db:
|
db:
|
||||||
imageName: "postgres:15"
|
imageName: "postgres:12"
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,6 @@ import org.springframework.context.annotation.Profile
|
||||||
import org.springframework.test.context.junit.jupiter.DisabledIf
|
import org.springframework.test.context.junit.jupiter.DisabledIf
|
||||||
|
|
||||||
@Profile(value = ["test", "test-slow"])
|
@Profile(value = ["test", "test-slow"])
|
||||||
|
@DisabledIf("\${tests.slow.disabled:false}")
|
||||||
@Target(allowedTargets = [AnnotationTarget.FUNCTION, AnnotationTarget.CLASS])
|
@Target(allowedTargets = [AnnotationTarget.FUNCTION, AnnotationTarget.CLASS])
|
||||||
@DisabledIf(
|
|
||||||
expression = """
|
|
||||||
#{systemProperties['tests.slow.enabled'] != null
|
|
||||||
? systemProperties['tests.slow.enabled'].toLowerCase().contains('false')
|
|
||||||
: false}
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
annotation class SlowTest
|
annotation class SlowTest
|
||||||
|
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
@file:Suppress("unused")
|
|
||||||
package ru.touchin.common.exceptions
|
|
||||||
|
|
||||||
open class CommonFormException(description: String?) : CommonException(description)
|
|
||||||
|
|
@ -73,11 +73,14 @@ complexity:
|
||||||
threshold: 40
|
threshold: 40
|
||||||
LongParameterList:
|
LongParameterList:
|
||||||
active: true
|
active: true
|
||||||
threshold: 10
|
functionThreshold: 10
|
||||||
ignoreDefaultParameters: false
|
ignoreDefaultParameters: false
|
||||||
MethodOverloading:
|
MethodOverloading:
|
||||||
active: false
|
active: false
|
||||||
threshold: 5
|
threshold: 5
|
||||||
|
NamedArguments:
|
||||||
|
active: false
|
||||||
|
threshold: 2
|
||||||
NestedBlockDepth:
|
NestedBlockDepth:
|
||||||
active: true
|
active: true
|
||||||
threshold: 4
|
threshold: 4
|
||||||
|
|
@ -290,6 +293,9 @@ style:
|
||||||
DataClassContainsFunctions:
|
DataClassContainsFunctions:
|
||||||
active: false
|
active: false
|
||||||
conversionFunctionPrefix: 'to'
|
conversionFunctionPrefix: 'to'
|
||||||
|
DestructuringDeclarationWithTooManyEntries:
|
||||||
|
active: true
|
||||||
|
maxDestructuringEntries: 5
|
||||||
EqualsNullCall:
|
EqualsNullCall:
|
||||||
active: true
|
active: true
|
||||||
ExplicitItLambdaParameter:
|
ExplicitItLambdaParameter:
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,536 @@
|
||||||
|
#### ENABLED RULES ####
|
||||||
|
|
||||||
|
# Checks that there is no more than one statement per line
|
||||||
|
- name: MORE_THAN_ONE_STATEMENT_PER_LINE
|
||||||
|
enabled: true
|
||||||
|
# Checks that line breaks follow code style guide: rule 3.6
|
||||||
|
- name: WRONG_NEWLINES
|
||||||
|
enabled: true
|
||||||
|
configuration:
|
||||||
|
# If the number of parameters on one line is more than this threshold, all parameters will be placed on separate lines.
|
||||||
|
maxParametersInOneLine: 2
|
||||||
|
# 3 by default.
|
||||||
|
maxCallsInOneLine: 2
|
||||||
|
# Checks that indentation is correct
|
||||||
|
- name: WRONG_INDENTATION
|
||||||
|
enabled: true
|
||||||
|
configuration:
|
||||||
|
# Is newline at the end of a file needed
|
||||||
|
newlineAtEnd: true
|
||||||
|
# If true: in parameter list when parameters are split by newline they are indented with two indentations instead of one
|
||||||
|
extendedIndentOfParameters: false
|
||||||
|
# If true: if first parameter in parameter list is on the same line as opening parenthesis, then other parameters can be aligned with it
|
||||||
|
alignedParameters: true
|
||||||
|
# If true, expression bodies which begin on a separate line are indented
|
||||||
|
# using a continuation indent. The default is false.
|
||||||
|
#
|
||||||
|
# This flag is called CONTINUATION_INDENT_FOR_EXPRESSION_BODIES in IDEA and
|
||||||
|
# ij_kotlin_continuation_indent_for_expression_bodies in .editorconfig.
|
||||||
|
extendedIndentForExpressionBodies: false
|
||||||
|
# If true: if expression is split by newline after operator like +/-/`*`, then the next line is indented with two indentations instead of one
|
||||||
|
extendedIndentAfterOperators: true
|
||||||
|
# If true: when dot qualified expression starts on a new line, this line will be indented with two indentations instead of one
|
||||||
|
extendedIndentBeforeDot: false
|
||||||
|
# The indentation size for each file
|
||||||
|
indentationSize: 4
|
||||||
|
# Checks that braces are used in if, else, when, for, do, and while statements. Exception: single line ternary operator statement
|
||||||
|
- name: NO_BRACES_IN_CONDITIONALS_AND_LOOPS
|
||||||
|
enabled: true
|
||||||
|
# Checks that annotation is on a single line
|
||||||
|
- name: ANNOTATION_NEW_LINE
|
||||||
|
enabled: true
|
||||||
|
# Checks that the long lambda has parameters
|
||||||
|
- name: TOO_MANY_LINES_IN_LAMBDA
|
||||||
|
enabled: true
|
||||||
|
configuration:
|
||||||
|
maxLambdaLength: 1 # max length of lambda without parameters
|
||||||
|
# Checks that the line length is < lineLength parameter
|
||||||
|
- name: LONG_LINE
|
||||||
|
enabled: true
|
||||||
|
configuration:
|
||||||
|
lineLength: '120'
|
||||||
|
# Checks trailing comma
|
||||||
|
- name: TRAILING_COMMA
|
||||||
|
enabled: true
|
||||||
|
configuration:
|
||||||
|
# VALUE_ARGUMENT
|
||||||
|
valueArgument: true
|
||||||
|
# VALUE_PARAMETER
|
||||||
|
valueParameter: true
|
||||||
|
# REFERENCE_EXPRESSION
|
||||||
|
indices: true
|
||||||
|
# WHEN_CONDITION_WITH_EXPRESSION
|
||||||
|
whenConditions: true
|
||||||
|
# STRING_TEMPLATE
|
||||||
|
collectionLiteral: true
|
||||||
|
# TYPE_PROJECTION
|
||||||
|
typeArgument: true
|
||||||
|
# TYPE_PARAMETER
|
||||||
|
typeParameter: true
|
||||||
|
# DESTRUCTURING_DECLARATION_ENTRY
|
||||||
|
destructuringDeclaration: true
|
||||||
|
|
||||||
|
#### DISABLED DEFAULT RULES ####
|
||||||
|
|
||||||
|
# Checks that the Class/Enum/Interface name matches Pascal case
|
||||||
|
- name: CLASS_NAME_INCORRECT
|
||||||
|
enabled: false
|
||||||
|
# all code blocks with MyAnnotation will be ignored and not checked
|
||||||
|
ignoreAnnotated: [ MyAnnotation ]
|
||||||
|
# Checks that CONSTANT (treated as const val from companion object or class level) is in non UPPER_SNAKE_CASE
|
||||||
|
- name: CONSTANT_UPPERCASE
|
||||||
|
enabled: false
|
||||||
|
# Checks that enum value is in upper SNAKE_CASE or in PascalCase depending on the config. UPPER_SNAKE_CASE is the default, but can be changed by 'enumStyle' config
|
||||||
|
- name: ENUM_VALUE
|
||||||
|
enabled: false
|
||||||
|
configuration:
|
||||||
|
# Two options: SNAKE_CASE (default), PascalCase
|
||||||
|
enumStyle: SNAKE_CASE
|
||||||
|
# Checks that class which extends any Exception class has Exception suffix
|
||||||
|
- name: EXCEPTION_SUFFIX
|
||||||
|
enabled: false
|
||||||
|
# Checks that file name has extension
|
||||||
|
- name: FILE_NAME_INCORRECT
|
||||||
|
enabled: false
|
||||||
|
# Checks that file name matches class name, if it is only one class in file
|
||||||
|
- name: FILE_NAME_MATCH_CLASS
|
||||||
|
enabled: false
|
||||||
|
# Checks that functions/methods which return boolean have special prefix like "is/should/e.t.c"
|
||||||
|
- name: FUNCTION_BOOLEAN_PREFIX
|
||||||
|
enabled: false
|
||||||
|
configuration:
|
||||||
|
allowedPrefixes: "" # A list of functions that return boolean and are allowed to use. Input is in a form "foo, bar".
|
||||||
|
# Checks that function/method name is in lowerCamelCase
|
||||||
|
- name: FUNCTION_NAME_INCORRECT_CASE
|
||||||
|
enabled: false
|
||||||
|
# Checks that typealias name is in PascalCase
|
||||||
|
- name: TYPEALIAS_NAME_INCORRECT_CASE
|
||||||
|
enabled: false
|
||||||
|
# Checks that generic name doesn't contain more than 1 letter (capital). It can be followed by numbers, example: T12, T
|
||||||
|
- name: GENERIC_NAME
|
||||||
|
enabled: false
|
||||||
|
# Identifier length should be in range [2,64] except names that used in industry like {i, j} and 'e' for catching exceptions
|
||||||
|
- name: IDENTIFIER_LENGTH
|
||||||
|
enabled: false
|
||||||
|
# Checks that the object matches PascalCase
|
||||||
|
- name: OBJECT_NAME_INCORRECT
|
||||||
|
enabled: false
|
||||||
|
# Checks that package name is in correct (lower) case
|
||||||
|
- name: PACKAGE_NAME_INCORRECT_CASE
|
||||||
|
enabled: false
|
||||||
|
# Checks that package name starts with the company's domain
|
||||||
|
- name: PACKAGE_NAME_INCORRECT_PREFIX
|
||||||
|
enabled: false
|
||||||
|
# Checks that package name does not have incorrect symbols like underscore or non-ASCII letters/digits
|
||||||
|
- name: PACKAGE_NAME_INCORRECT_SYMBOLS
|
||||||
|
enabled: false
|
||||||
|
# Checks that the path for a file matches with a package name
|
||||||
|
- name: PACKAGE_NAME_INCORRECT_PATH
|
||||||
|
enabled: false
|
||||||
|
# Checks that package name is in the file
|
||||||
|
- name: PACKAGE_NAME_MISSING
|
||||||
|
enabled: false
|
||||||
|
# Checks that variable does not have prefix (like mVariable or M_VARIABLE)
|
||||||
|
- name: VARIABLE_HAS_PREFIX
|
||||||
|
enabled: false
|
||||||
|
# Checks that variable does not contain one single letter, only exceptions are fixed names that used in industry like {i, j}
|
||||||
|
- name: VARIABLE_NAME_INCORRECT
|
||||||
|
enabled: false
|
||||||
|
# Checks that the name of variable is in lowerCamelCase and contains only ASCII letters
|
||||||
|
- name: VARIABLE_NAME_INCORRECT_FORMAT
|
||||||
|
enabled: false
|
||||||
|
# Checks that functions have kdoc
|
||||||
|
- name: MISSING_KDOC_ON_FUNCTION
|
||||||
|
enabled: false
|
||||||
|
# Checks that on file level internal or public class or function has missing KDoc
|
||||||
|
- name: MISSING_KDOC_TOP_LEVEL
|
||||||
|
enabled: false
|
||||||
|
# Checks that accessible internal elements (protected, public, internal) in a class are documented
|
||||||
|
- name: MISSING_KDOC_CLASS_ELEMENTS
|
||||||
|
enabled: false
|
||||||
|
# Checks that accessible method parameters are documented in KDoc
|
||||||
|
- name: KDOC_WITHOUT_PARAM_TAG
|
||||||
|
enabled: false
|
||||||
|
# Checks that accessible method explicit return type is documented in KDoc
|
||||||
|
- name: KDOC_WITHOUT_RETURN_TAG
|
||||||
|
enabled: false
|
||||||
|
# Checks that accessible method throw keyword is documented in KDoc
|
||||||
|
- name: KDOC_WITHOUT_THROWS_TAG
|
||||||
|
enabled: false
|
||||||
|
# Checks that KDoc is not empty
|
||||||
|
- name: KDOC_EMPTY_KDOC
|
||||||
|
enabled: false
|
||||||
|
# Checks that underscore is correctly used to split package naming
|
||||||
|
- name: INCORRECT_PACKAGE_SEPARATOR
|
||||||
|
enabled: false
|
||||||
|
# Checks that code block doesn't contain kdoc comments
|
||||||
|
- name: COMMENTED_BY_KDOC
|
||||||
|
enabled: false
|
||||||
|
# Checks that there is no @deprecated tag in kdoc
|
||||||
|
- name: KDOC_NO_DEPRECATED_TAG
|
||||||
|
enabled: false
|
||||||
|
# Checks that there is no empty content in kdoc tags
|
||||||
|
- name: KDOC_NO_EMPTY_TAGS
|
||||||
|
enabled: false
|
||||||
|
# Checks that there is only one space after kdoc tag
|
||||||
|
- name: KDOC_WRONG_SPACES_AFTER_TAG
|
||||||
|
enabled: false
|
||||||
|
# Checks tags order in kDoc. `@param`, `@return`, `@throws`
|
||||||
|
- name: KDOC_WRONG_TAGS_ORDER
|
||||||
|
enabled: false
|
||||||
|
# Checks that there is no newline of empty KDoc line (with leading asterisk) between `@param`, `@return`, `@throws` tags
|
||||||
|
- name: KDOC_NO_NEWLINES_BETWEEN_BASIC_TAGS
|
||||||
|
enabled: false
|
||||||
|
# Checks that block of tags @param, @return, @throws is separated from previous part of KDoc by exactly one empty line
|
||||||
|
- name: KDOC_NEWLINES_BEFORE_BASIC_TAGS
|
||||||
|
enabled: false
|
||||||
|
# Checks that special tags `@apiNote`, `@implNote`, `@implSpec` have exactly one empty line after
|
||||||
|
- name: KDOC_NO_NEWLINE_AFTER_SPECIAL_TAGS
|
||||||
|
enabled: false
|
||||||
|
# Checks that kdoc does not contain @author tag or date
|
||||||
|
- name: KDOC_CONTAINS_DATE_OR_AUTHOR
|
||||||
|
enabled: false
|
||||||
|
configuration:
|
||||||
|
versionRegex: \d+\.\d+\.\d+[-.\w\d]*
|
||||||
|
# Checks that KDoc does not contain single line with words 'return', 'get' or 'set'
|
||||||
|
- name: KDOC_TRIVIAL_KDOC_ON_FUNCTION
|
||||||
|
enabled: false
|
||||||
|
# Checks that there is newline after header KDoc
|
||||||
|
- name: HEADER_WRONG_FORMAT
|
||||||
|
enabled: false
|
||||||
|
# Checks that file with zero or >1 classes has header KDoc
|
||||||
|
- name: HEADER_MISSING_IN_NON_SINGLE_CLASS_FILE
|
||||||
|
enabled: false
|
||||||
|
# Checks that copyright exists on top of file and is properly formatted (as a block comment)
|
||||||
|
- name: HEADER_MISSING_OR_WRONG_COPYRIGHT
|
||||||
|
enabled: false
|
||||||
|
configuration:
|
||||||
|
isCopyrightMandatory: false
|
||||||
|
copyrightText: 'Copyright (c) Your Company Name Here. 2010-;@currYear;'
|
||||||
|
# Checks that header kdoc is located before package directive
|
||||||
|
- name: HEADER_NOT_BEFORE_PACKAGE
|
||||||
|
enabled: false
|
||||||
|
# Checks that file does not contain lines > maxSize
|
||||||
|
- name: FILE_IS_TOO_LONG
|
||||||
|
enabled: false
|
||||||
|
configuration:
|
||||||
|
# number of lines
|
||||||
|
maxSize: '2000'
|
||||||
|
# Checks that file does not contain commented out code
|
||||||
|
- name: COMMENTED_OUT_CODE
|
||||||
|
enabled: false
|
||||||
|
# Checks that file does not contain only comments, imports and package directive
|
||||||
|
- name: FILE_CONTAINS_ONLY_COMMENTS
|
||||||
|
enabled: false
|
||||||
|
# Orders imports alphabetically
|
||||||
|
- name: FILE_UNORDERED_IMPORTS
|
||||||
|
enabled: false
|
||||||
|
configuration:
|
||||||
|
# use logical imports grouping with sorting inside of a group
|
||||||
|
useRecommendedImportsOrder: true
|
||||||
|
# Checks that general order of code parts is right
|
||||||
|
- name: FILE_INCORRECT_BLOCKS_ORDER
|
||||||
|
enabled: false
|
||||||
|
# Checks that there is exactly one line between code blocks
|
||||||
|
- name: FILE_NO_BLANK_LINE_BETWEEN_BLOCKS
|
||||||
|
enabled: false
|
||||||
|
# Checks that there is no wildcard imports. Exception: allowedWildcards
|
||||||
|
- name: FILE_WILDCARD_IMPORTS
|
||||||
|
enabled: false
|
||||||
|
configuration:
|
||||||
|
allowedWildcards: "" # Allowed wildcards for imports (e.g. "import org.cqfn.diktat.*, import org.jetbrains.kotlin.*")
|
||||||
|
useRecommendedImportsOrder: true
|
||||||
|
# Checks unused imports
|
||||||
|
- name: UNUSED_IMPORT
|
||||||
|
enabled: false
|
||||||
|
configuration:
|
||||||
|
deleteUnusedImport: true
|
||||||
|
# Checks that the declaration part of a class-like code structures (class/interface/etc.) is in the proper order
|
||||||
|
- name: WRONG_ORDER_IN_CLASS_LIKE_STRUCTURES
|
||||||
|
enabled: false
|
||||||
|
# Checks that properties with comments are separated by a blank line
|
||||||
|
- name: BLANK_LINE_BETWEEN_PROPERTIES
|
||||||
|
enabled: false
|
||||||
|
# Checks top level order
|
||||||
|
- name: TOP_LEVEL_ORDER
|
||||||
|
enabled: false
|
||||||
|
# Checks that non-empty code blocks with braces follow the K&R style (1TBS or OTBS style)
|
||||||
|
- name: BRACES_BLOCK_STRUCTURE_ERROR
|
||||||
|
enabled: false
|
||||||
|
configuration:
|
||||||
|
openBraceNewline: 'True'
|
||||||
|
closeBraceNewline: 'True'
|
||||||
|
# Checks that there is no empty blocks in a file.
|
||||||
|
# If allowEmptyBlocks is true, checks that it follows correct style (have a newline)
|
||||||
|
- name: EMPTY_BLOCK_STRUCTURE_ERROR
|
||||||
|
enabled: false
|
||||||
|
configuration:
|
||||||
|
# Whether a newline after `{` is required in an empty block
|
||||||
|
styleEmptyBlockWithNewline: 'True'
|
||||||
|
allowEmptyBlocks: 'False'
|
||||||
|
# Checks that semicolons are not used at the end of a line
|
||||||
|
- name: REDUNDANT_SEMICOLON
|
||||||
|
enabled: false
|
||||||
|
# Checks that there are not too many consecutive spaces in line
|
||||||
|
- name: TOO_MANY_CONSECUTIVE_SPACES
|
||||||
|
enabled: false
|
||||||
|
configuration:
|
||||||
|
# Maximum allowed number of consecutive spaces (not counting indentation)
|
||||||
|
maxSpaces: '1'
|
||||||
|
# Whether formatting for enums should be kept without checking
|
||||||
|
saveInitialFormattingForEnums: false
|
||||||
|
# Inspection that checks if a long dot qualified expression is used in condition or as an argument
|
||||||
|
- name: COMPLEX_EXPRESSION
|
||||||
|
enabled: false
|
||||||
|
# Checks that blank lines are used correctly.
|
||||||
|
# For example: triggers when there are too many blank lines between function declaration
|
||||||
|
- name: TOO_MANY_BLANK_LINES
|
||||||
|
enabled: false
|
||||||
|
# Checks that usage of horizontal spaces doesn't violate code style guide
|
||||||
|
- name: WRONG_WHITESPACE
|
||||||
|
enabled: false
|
||||||
|
# Checks that backticks (``) are not used in the identifier name, except the case when it is test method (marked with @Test annotation)
|
||||||
|
- name: BACKTICKS_PROHIBITED
|
||||||
|
enabled: false
|
||||||
|
# Checks that a single line concatenation of strings is not used
|
||||||
|
- name: STRING_CONCATENATION
|
||||||
|
enabled: false
|
||||||
|
# Checks that each when statement have else in the end
|
||||||
|
- name: WHEN_WITHOUT_ELSE
|
||||||
|
enabled: false
|
||||||
|
# Checks that enum structure is correct: enum entries should be separated by comma and line break and last entry should have semicolon in the end.
|
||||||
|
- name: ENUMS_SEPARATED
|
||||||
|
enabled: false
|
||||||
|
# Checks that value on integer or float constant is not too big
|
||||||
|
- name: LONG_NUMERICAL_VALUES_SEPARATED
|
||||||
|
enabled: false
|
||||||
|
configuration:
|
||||||
|
# Maximum number of digits which are not split
|
||||||
|
maxNumberLength: '5'
|
||||||
|
# Maximum number of digits between separators
|
||||||
|
maxBlockLength: '3'
|
||||||
|
# Checks magic number
|
||||||
|
- name: MAGIC_NUMBER
|
||||||
|
enabled: false
|
||||||
|
configuration:
|
||||||
|
# Ignore numbers from test
|
||||||
|
ignoreTest: "true"
|
||||||
|
# Ignore numbers
|
||||||
|
ignoreNumbers: "-1, 1, 0, 2, 0U, 1U, 2U, -1L, 0L, 1L, 2L, 0UL, 1UL, 2UL"
|
||||||
|
# Is ignore override hashCode function
|
||||||
|
ignoreHashCodeFunction: "true"
|
||||||
|
# Is ignore property
|
||||||
|
ignorePropertyDeclaration: "false"
|
||||||
|
# Is ignore local variable
|
||||||
|
ignoreLocalVariableDeclaration: "false"
|
||||||
|
# Is ignore constant
|
||||||
|
ignoreConstantDeclaration: "true"
|
||||||
|
# Is ignore property in companion object
|
||||||
|
ignoreCompanionObjectPropertyDeclaration: "true"
|
||||||
|
# Is ignore numbers in enum
|
||||||
|
ignoreEnums: "false"
|
||||||
|
# Is ignore number in ranges
|
||||||
|
ignoreRanges: "false"
|
||||||
|
# Is ignore number in extension function
|
||||||
|
ignoreExtensionFunctions: "false"
|
||||||
|
# Checks that order of enum values or constant property inside companion is correct
|
||||||
|
- name: WRONG_DECLARATIONS_ORDER
|
||||||
|
enabled: false
|
||||||
|
configuration:
|
||||||
|
# Whether enum members should be sorted alphabetically
|
||||||
|
sortEnum: true
|
||||||
|
# Whether class properties should be sorted alphabetically
|
||||||
|
sortProperty: true
|
||||||
|
# Checks that multiple modifiers sequence is in the correct order
|
||||||
|
- name: WRONG_MULTIPLE_MODIFIERS_ORDER
|
||||||
|
enabled: false
|
||||||
|
# Checks that identifier has appropriate name (See table of rule 1.2 part 6)
|
||||||
|
- name: CONFUSING_IDENTIFIER_NAMING
|
||||||
|
enabled: false
|
||||||
|
# Checks year in the copyright
|
||||||
|
- name: WRONG_COPYRIGHT_YEAR
|
||||||
|
enabled: false
|
||||||
|
# Inspection that checks if local variables are declared close to the first usage site
|
||||||
|
- name: LOCAL_VARIABLE_EARLY_DECLARATION
|
||||||
|
enabled: false
|
||||||
|
# Try to avoid initialize val by null (e.g. val a: Int? = null -> val a: Int = 0)
|
||||||
|
- name: NULLABLE_PROPERTY_TYPE
|
||||||
|
enabled: false
|
||||||
|
# Inspection that checks if there is a blank line before kDoc and none after
|
||||||
|
- name: WRONG_NEWLINES_AROUND_KDOC
|
||||||
|
enabled: false
|
||||||
|
# Inspection that checks if there is no blank lines before first comment
|
||||||
|
- name: FIRST_COMMENT_NO_BLANK_LINE
|
||||||
|
enabled: false
|
||||||
|
# Inspection that checks if there are blank lines between code and comment and between code start token and comment's text
|
||||||
|
- name: COMMENT_WHITE_SPACE
|
||||||
|
enabled: false
|
||||||
|
configuration:
|
||||||
|
maxSpacesBeforeComment: 2
|
||||||
|
maxSpacesInComment: 1
|
||||||
|
# Inspection that checks if all comment's are inside if-else code blocks. Exception is general if comment
|
||||||
|
- name: IF_ELSE_COMMENTS
|
||||||
|
enabled: false
|
||||||
|
# Type aliases provide alternative names for existing types when type's reference text is longer 25 chars
|
||||||
|
- name: TYPE_ALIAS
|
||||||
|
enabled: false
|
||||||
|
configuration:
|
||||||
|
typeReferenceLength: '25' # max length of type reference
|
||||||
|
# Checks if casting can be omitted
|
||||||
|
- name: SMART_CAST_NEEDED
|
||||||
|
enabled: false
|
||||||
|
# Checks that variables of generic types have explicit type declaration
|
||||||
|
- name: GENERIC_VARIABLE_WRONG_DECLARATION
|
||||||
|
enabled: false
|
||||||
|
# Inspection that checks if string template has redundant curly braces
|
||||||
|
- name: STRING_TEMPLATE_CURLY_BRACES
|
||||||
|
enabled: false
|
||||||
|
# Variables with `val` modifier - are immutable (read-only). Usage of such variables instead of `var` variables increases
|
||||||
|
# robustness and readability of code, because `var` variables can be reassigned several times in the business logic.
|
||||||
|
# This rule prohibits usage of `var`s as local variables - the only exception is accumulators and counters
|
||||||
|
- name: SAY_NO_TO_VAR
|
||||||
|
enabled: false
|
||||||
|
# Inspection that checks if string template has redundant quotes
|
||||||
|
- name: STRING_TEMPLATE_QUOTES
|
||||||
|
enabled: false
|
||||||
|
# Check if there are redundant nested if-statements, which could be collapsed into a single one by concatenating their conditions
|
||||||
|
- name: COLLAPSE_IF_STATEMENTS
|
||||||
|
enabled: false
|
||||||
|
configuration:
|
||||||
|
startCollapseFromNestedLevel: 2
|
||||||
|
# Checks that floating-point values are not used in arithmetic expressions
|
||||||
|
- name: FLOAT_IN_ACCURATE_CALCULATIONS
|
||||||
|
enabled: false
|
||||||
|
# Checks that function length isn't too long
|
||||||
|
- name: TOO_LONG_FUNCTION
|
||||||
|
enabled: false
|
||||||
|
configuration:
|
||||||
|
maxFunctionLength: '30' # max length of function
|
||||||
|
isIncludeHeader: 'false' # count function's header
|
||||||
|
# Warns if there are nested functions
|
||||||
|
- name: AVOID_NESTED_FUNCTIONS
|
||||||
|
enabled: false
|
||||||
|
# Checks that lambda inside function parameters is in the end
|
||||||
|
- name: LAMBDA_IS_NOT_LAST_PARAMETER
|
||||||
|
enabled: false
|
||||||
|
# Checks that function doesn't contains too many parameters
|
||||||
|
- name: TOO_MANY_PARAMETERS
|
||||||
|
enabled: false
|
||||||
|
configuration:
|
||||||
|
maxParameterListSize: '5' # max parameters size
|
||||||
|
# Checks that function doesn't have too many nested blocks
|
||||||
|
- name: NESTED_BLOCK
|
||||||
|
enabled: false
|
||||||
|
configuration:
|
||||||
|
maxNestedBlockQuantity: '4'
|
||||||
|
# Checks that function use default values, instead overloading
|
||||||
|
- name: WRONG_OVERLOADING_FUNCTION_ARGUMENTS
|
||||||
|
enabled: false
|
||||||
|
# Checks that using runBlocking inside async block code
|
||||||
|
- name: RUN_BLOCKING_INSIDE_ASYNC
|
||||||
|
enabled: false
|
||||||
|
# Checks that property in constructor doesn't contain comment
|
||||||
|
- name: KDOC_NO_CONSTRUCTOR_PROPERTY
|
||||||
|
enabled: false
|
||||||
|
# Checks that using unnecessary, custom label
|
||||||
|
- name: CUSTOM_LABEL
|
||||||
|
enabled: false
|
||||||
|
# Check that lambda with inner lambda doesn't use implicit parameter
|
||||||
|
- name: PARAMETER_NAME_IN_OUTER_LAMBDA
|
||||||
|
enabled: false
|
||||||
|
# Checks that property in KDoc present in class
|
||||||
|
- name: KDOC_EXTRA_PROPERTY
|
||||||
|
enabled: false
|
||||||
|
# There's a property in KDoc which is already present
|
||||||
|
- name: KDOC_DUPLICATE_PROPERTY
|
||||||
|
enabled: false
|
||||||
|
# Checks that KDoc in constructor has property tag but with comment inside constructor
|
||||||
|
- name: KDOC_NO_CONSTRUCTOR_PROPERTY_WITH_COMMENT
|
||||||
|
enabled: false
|
||||||
|
# if a class has single constructor, it should be converted to a primary constructor
|
||||||
|
- name: SINGLE_CONSTRUCTOR_SHOULD_BE_PRIMARY
|
||||||
|
enabled: false
|
||||||
|
# Checks if class can be made as data class
|
||||||
|
- name: USE_DATA_CLASS
|
||||||
|
enabled: false
|
||||||
|
# Checks that never use the name of a variable in the custom getter or setter
|
||||||
|
- name: WRONG_NAME_OF_VARIABLE_INSIDE_ACCESSOR
|
||||||
|
enabled: false
|
||||||
|
# Checks that classes have only one init block
|
||||||
|
- name: MULTIPLE_INIT_BLOCKS
|
||||||
|
enabled: false
|
||||||
|
# Checks that there are abstract functions in abstract class
|
||||||
|
- name: CLASS_SHOULD_NOT_BE_ABSTRACT
|
||||||
|
enabled: false
|
||||||
|
# Checks if there are any trivial getters or setters
|
||||||
|
- name: TRIVIAL_ACCESSORS_ARE_NOT_RECOMMENDED
|
||||||
|
enabled: false
|
||||||
|
# Checks that no custom getters and setters are used for properties. It is a more wide rule than TRIVIAL_ACCESSORS_ARE_NOT_RECOMMENDED
|
||||||
|
# Kotlin compiler automatically generates `get` and `set` methods for properties and also lets the possibility to override it.
|
||||||
|
# But in all cases it is very confusing when `get` and `set` are overridden for a developer who uses this particular class.
|
||||||
|
# Developer expects to get the value of the property, but receives some unknown value and some extra side effect hidden by the custom getter/setter.
|
||||||
|
# Use extra functions for it instead.
|
||||||
|
- name: CUSTOM_GETTERS_SETTERS
|
||||||
|
enabled: false
|
||||||
|
# Checks if null-check was used explicitly (for example: if (a == null))
|
||||||
|
# Try to avoid explicit null checks (explicit comparison with `null`)
|
||||||
|
# Kotlin is declared as [Null-safe](https://kotlinlang.org/docs/reference/null-safety.html) language.
|
||||||
|
# But Kotlin architects wanted Kotlin to be fully compatible with Java, that's why `null` keyword was also introduced in Kotlin.
|
||||||
|
# There are several code-structures that can be used in Kotlin to avoid null-checks. For example: `?:`, `.let {}`, `.also {}`, e.t.c
|
||||||
|
- name: AVOID_NULL_CHECKS
|
||||||
|
enabled: false
|
||||||
|
# Checks if class instantiation can be wrapped in `apply` for better readability
|
||||||
|
- name: COMPACT_OBJECT_INITIALIZATION
|
||||||
|
enabled: false
|
||||||
|
# Checks explicit supertype qualification
|
||||||
|
- name: USELESS_SUPERTYPE
|
||||||
|
enabled: false
|
||||||
|
# Checks if extension function with the same signature don't have related classes
|
||||||
|
- name: EXTENSION_FUNCTION_SAME_SIGNATURE
|
||||||
|
enabled: false
|
||||||
|
# Checks if there is empty primary constructor
|
||||||
|
- name: EMPTY_PRIMARY_CONSTRUCTOR
|
||||||
|
enabled: false
|
||||||
|
# In case of not using field keyword in property accessors,
|
||||||
|
# there should be explicit backing property with the name of real property
|
||||||
|
# Example: val table get() {if (_table == null) ...} -> table should have _table
|
||||||
|
- name: NO_CORRESPONDING_PROPERTY
|
||||||
|
enabled: false
|
||||||
|
# Checks if there is class/object that can be replace with extension function
|
||||||
|
- name: AVOID_USING_UTILITY_CLASS
|
||||||
|
enabled: false
|
||||||
|
# If there is stateless class it is preferred to use object
|
||||||
|
- name: OBJECT_IS_PREFERRED
|
||||||
|
enabled: false
|
||||||
|
# If there exists negated version of function you should prefer it instead of !functionCall
|
||||||
|
- name: INVERSE_FUNCTION_PREFERRED
|
||||||
|
enabled: false
|
||||||
|
# Checks if class can be converted to inline class
|
||||||
|
- name: INLINE_CLASS_CAN_BE_USED
|
||||||
|
enabled: false
|
||||||
|
# If file contains class, then it can't contain extension functions for the same class
|
||||||
|
- name: EXTENSION_FUNCTION_WITH_CLASS
|
||||||
|
enabled: false
|
||||||
|
# Check if kts script contains other functions except run code
|
||||||
|
- name: RUN_IN_SCRIPT
|
||||||
|
enabled: false
|
||||||
|
# Check if boolean expression can be simplified
|
||||||
|
- name: COMPLEX_BOOLEAN_EXPRESSION
|
||||||
|
enabled: false
|
||||||
|
# Check if range can replace with until or `rangeTo` function with range
|
||||||
|
- name: CONVENTIONAL_RANGE
|
||||||
|
enabled: false
|
||||||
|
configuration:
|
||||||
|
isRangeToIgnore: false
|
||||||
|
# Check if there is a call of print()\println() or console.log(). Assumption that it's a debug print
|
||||||
|
- name: DEBUG_PRINT
|
||||||
|
enabled: false
|
||||||
|
# Check that typealias name is in PascalCase
|
||||||
|
- name: TYPEALIAS_NAME_INCORRECT_CASE
|
||||||
|
enabled: false
|
||||||
|
# Should change property length - 1 to property lastIndex
|
||||||
|
- name: USE_LAST_INDEX
|
||||||
|
enabled: false
|
||||||
|
# Only properties from the primary constructor should be documented in a @property tag in class KDoc
|
||||||
|
- name: KDOC_NO_CLASS_BODY_PROPERTIES_IN_HEADER
|
||||||
|
enabled: false
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
kotlin.code.style=official
|
kotlin.code.style=official
|
||||||
springBootVersion=2.5.3
|
springBootVersion=2.5.3
|
||||||
springDependencyManagementVersion=1.0.11.RELEASE
|
springDependencyManagementVersion=1.0.11.RELEASE
|
||||||
|
detektGradlePluginVersion=1.18.0
|
||||||
|
diktatGradlePluginVersion=1.2.5
|
||||||
kotlinVersion=1.6.21
|
kotlinVersion=1.6.21
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
package ru.touchin.logger.dto
|
package ru.touchin.logger.dto
|
||||||
|
|
||||||
enum class LogLevel {
|
enum class LogLevel {
|
||||||
Trace, Info, Error
|
Trace, Debug, Info, Error
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,14 @@ abstract class AbstractLog(clazz: Class<*>) : Log<LogData> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun debug() {
|
||||||
|
if (logger.isDebugEnabled) {
|
||||||
|
val logMessage = getMessage()
|
||||||
|
|
||||||
|
logger.debug(logMessage.message, logMessage.error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun info() {
|
override fun info() {
|
||||||
if (logger.isInfoEnabled) {
|
if (logger.isInfoEnabled) {
|
||||||
val logMessage = getMessage()
|
val logMessage = getMessage()
|
||||||
|
|
@ -58,6 +66,7 @@ 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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ interface Log<T> {
|
||||||
|
|
||||||
var logData: T
|
var logData: T
|
||||||
fun trace()
|
fun trace()
|
||||||
|
fun debug()
|
||||||
fun info()
|
fun info()
|
||||||
fun error()
|
fun error()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,4 @@ package ru.touchin.auth.core.scope.dto
|
||||||
|
|
||||||
data class Scope(
|
data class Scope(
|
||||||
val name: String
|
val name: String
|
||||||
) {
|
)
|
||||||
|
|
||||||
companion object {
|
|
||||||
val user = Scope("user")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -2,41 +2,35 @@ package ru.touchin.auth.core.tokens.access.config
|
||||||
|
|
||||||
import com.auth0.jwt.algorithms.Algorithm
|
import com.auth0.jwt.algorithms.Algorithm
|
||||||
import org.springframework.beans.factory.annotation.Qualifier
|
import org.springframework.beans.factory.annotation.Qualifier
|
||||||
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.Configuration
|
||||||
import org.springframework.context.annotation.Import
|
import org.springframework.context.annotation.Import
|
||||||
import ru.touchin.auth.core.tokens.access.properties.AccessTokenProperties
|
import ru.touchin.auth.core.tokens.access.properties.AccessTokenProperties
|
||||||
import ru.touchin.auth.security.jwt.configurations.JwtConfiguration
|
import ru.touchin.auth.security.jwt.configurations.JwtConfiguration
|
||||||
import ru.touchin.auth.security.jwt.utils.JwtUtils.getKeySpec
|
import ru.touchin.auth.security.jwt.utils.JwtUtils.getKeySpec
|
||||||
import java.io.File
|
|
||||||
import java.security.KeyFactory
|
import java.security.KeyFactory
|
||||||
import java.security.interfaces.RSAPrivateKey
|
import java.security.interfaces.RSAPrivateKey
|
||||||
import java.security.interfaces.RSAPublicKey
|
import java.security.interfaces.RSAPublicKey
|
||||||
import java.security.spec.PKCS8EncodedKeySpec
|
import java.security.spec.PKCS8EncodedKeySpec
|
||||||
|
|
||||||
|
@Configuration
|
||||||
@Import(JwtConfiguration::class)
|
@Import(JwtConfiguration::class)
|
||||||
@ComponentScan("ru.touchin.auth.core.tokens.access")
|
|
||||||
@ConfigurationPropertiesScan("ru.touchin.auth.core.tokens.access")
|
|
||||||
class AccessTokenBeanConfig(private val accessTokenProperties: AccessTokenProperties) {
|
class AccessTokenBeanConfig(private val accessTokenProperties: AccessTokenProperties) {
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
fun accessTokenSigningAlgorithm(
|
fun accessTokenSigningAlgorithm(
|
||||||
@Qualifier("accessTokenPublicKey")
|
@Qualifier("accessTokenPublicKey")
|
||||||
accessTokenPublicKey: RSAPublicKey,
|
accessTokenPublicKey: RSAPublicKey
|
||||||
@Qualifier("accessTokenPrivateKey")
|
|
||||||
accessTokenPrivateKey: RSAPrivateKey,
|
|
||||||
): Algorithm {
|
): Algorithm {
|
||||||
return Algorithm.RSA256(
|
return Algorithm.RSA256(
|
||||||
accessTokenPublicKey,
|
accessTokenPublicKey,
|
||||||
accessTokenPrivateKey,
|
accessTokenPrivateKey()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean("accessTokenPrivateKey")
|
@Bean("accessTokenPrivateKey")
|
||||||
fun accessTokenPrivateKey(): RSAPrivateKey {
|
fun accessTokenPrivateKey(): RSAPrivateKey {
|
||||||
val privateKey = this.javaClass.classLoader.getResource(accessTokenProperties.keyPair.private)!!.readText()
|
val keySpecPKCS8 = getKeySpec(accessTokenProperties.keyPair.private, ::PKCS8EncodedKeySpec)
|
||||||
val keySpecPKCS8 = getKeySpec(privateKey, ::PKCS8EncodedKeySpec)
|
|
||||||
|
|
||||||
return keyFactory.generatePrivate(keySpecPKCS8) as RSAPrivateKey
|
return keyFactory.generatePrivate(keySpecPKCS8) as RSAPrivateKey
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import org.springframework.web.bind.annotation.GetMapping
|
||||||
import org.springframework.web.bind.annotation.RequestMapping
|
import org.springframework.web.bind.annotation.RequestMapping
|
||||||
import org.springframework.web.bind.annotation.RestController
|
import org.springframework.web.bind.annotation.RestController
|
||||||
import org.springframework.web.util.DefaultUriBuilderFactory
|
import org.springframework.web.util.DefaultUriBuilderFactory
|
||||||
|
import ru.touchin.auth.core.tokens.access.properties.AccessTokenProperties
|
||||||
import ru.touchin.auth.security.oauth2.metadata.properties.OAuth2MetadataProperties
|
import ru.touchin.auth.security.oauth2.metadata.properties.OAuth2MetadataProperties
|
||||||
import ru.touchin.auth.security.oauth2.metadata.response.OAuth2MetadataResponse
|
import ru.touchin.auth.security.oauth2.metadata.response.OAuth2MetadataResponse
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
|
|
@ -14,12 +15,13 @@ import java.net.URI
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/.well-known/oauth-authorization-server")
|
@RequestMapping("/.well-known/oauth-authorization-server")
|
||||||
class OAuth2MetadataController(
|
class OAuth2MetadataController(
|
||||||
|
private val accessTokenProperties: AccessTokenProperties,
|
||||||
private val oauth2MetadataProperties: OAuth2MetadataProperties,
|
private val oauth2MetadataProperties: OAuth2MetadataProperties,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
fun metadata(): OAuth2MetadataResponse {
|
fun metadata(): OAuth2MetadataResponse {
|
||||||
val issuer = oauth2MetadataProperties.issuer.let(URI::create)
|
val issuer = accessTokenProperties.issuer.let(URI::create)
|
||||||
|
|
||||||
return OAuth2MetadataResponse(
|
return OAuth2MetadataResponse(
|
||||||
issuer = issuer,
|
issuer = issuer,
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import org.springframework.boot.context.properties.ConstructorBinding
|
||||||
@ConstructorBinding
|
@ConstructorBinding
|
||||||
@ConfigurationProperties(prefix = "oauth2-metadata")
|
@ConfigurationProperties(prefix = "oauth2-metadata")
|
||||||
data class OAuth2MetadataProperties(
|
data class OAuth2MetadataProperties(
|
||||||
val issuer: String,
|
|
||||||
val tokenEndpoint: String?,
|
val tokenEndpoint: String?,
|
||||||
val responseTypesSupported: List<String> = emptyList(),
|
val responseTypesSupported: List<String> = emptyList(),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import org.springframework.context.annotation.Bean
|
||||||
import org.springframework.context.annotation.ComponentScan
|
import org.springframework.context.annotation.ComponentScan
|
||||||
import ru.touchin.auth.security.jwt.properties.AccessTokenPublicProperties
|
import ru.touchin.auth.security.jwt.properties.AccessTokenPublicProperties
|
||||||
import ru.touchin.auth.security.jwt.utils.JwtUtils.getKeySpec
|
import ru.touchin.auth.security.jwt.utils.JwtUtils.getKeySpec
|
||||||
import java.io.File
|
|
||||||
import java.security.KeyFactory
|
import java.security.KeyFactory
|
||||||
import java.security.interfaces.RSAPublicKey
|
import java.security.interfaces.RSAPublicKey
|
||||||
import java.security.spec.X509EncodedKeySpec
|
import java.security.spec.X509EncodedKeySpec
|
||||||
|
|
@ -21,8 +20,7 @@ class JwtConfiguration {
|
||||||
fun accessTokenPublicKey(
|
fun accessTokenPublicKey(
|
||||||
accessTokenPublicProperties: AccessTokenPublicProperties,
|
accessTokenPublicProperties: AccessTokenPublicProperties,
|
||||||
): RSAPublicKey {
|
): RSAPublicKey {
|
||||||
val publicKey = this.javaClass.classLoader.getResource(accessTokenPublicProperties.keyPair.public)!!.readText()
|
val keySpecX509 = getKeySpec(accessTokenPublicProperties.keyPair.public, ::X509EncodedKeySpec)
|
||||||
val keySpecX509 = getKeySpec(publicKey, ::X509EncodedKeySpec)
|
|
||||||
|
|
||||||
return keyFactory.generatePublic(keySpecX509) as RSAPublicKey
|
return keyFactory.generatePublic(keySpecX509) as RSAPublicKey
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ package ru.touchin.auth.security.jwt.http.configurators
|
||||||
|
|
||||||
import org.springframework.core.annotation.Order
|
import org.springframework.core.annotation.Order
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||||
import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver
|
|
||||||
import org.springframework.stereotype.Component
|
import org.springframework.stereotype.Component
|
||||||
import ru.touchin.common.spring.Ordered
|
import ru.touchin.common.spring.Ordered
|
||||||
import ru.touchin.common.spring.security.http.configurators.HttpSecurityConfigurator
|
import ru.touchin.common.spring.security.http.configurators.HttpSecurityConfigurator
|
||||||
|
|
@ -12,10 +11,9 @@ import ru.touchin.common.spring.security.http.configurators.HttpSecurityConfigur
|
||||||
class JwtHttpSecurityConfigurator : HttpSecurityConfigurator {
|
class JwtHttpSecurityConfigurator : HttpSecurityConfigurator {
|
||||||
|
|
||||||
override fun configure(http: HttpSecurity) {
|
override fun configure(http: HttpSecurity) {
|
||||||
val resolver = DefaultBearerTokenResolver()
|
http.oauth2ResourceServer {
|
||||||
resolver.setAllowUriQueryParameter(true)
|
it.jwt()
|
||||||
|
}
|
||||||
http.oauth2ResourceServer().bearerTokenResolver(resolver).jwt()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,15 @@ pluginManagement {
|
||||||
val kotlinVersion: String by settings
|
val kotlinVersion: String by settings
|
||||||
val springBootVersion: String by settings
|
val springBootVersion: String by settings
|
||||||
val springDependencyManagementVersion: String by settings
|
val springDependencyManagementVersion: String by settings
|
||||||
|
val detektGradlePluginVersion: String by settings
|
||||||
|
val diktatGradlePluginVersion: String by settings
|
||||||
|
|
||||||
val versions = listOf(
|
val versions = listOf(
|
||||||
"org.jetbrains.kotlin" to kotlinVersion,
|
"org.jetbrains.kotlin" to kotlinVersion,
|
||||||
"org.springframework.boot" to springBootVersion,
|
"org.springframework.boot" to springBootVersion,
|
||||||
"io.spring.dependency-management" to springDependencyManagementVersion
|
"io.spring.dependency-management" to springDependencyManagementVersion,
|
||||||
|
"io.gitlab.arturbosch.detekt" to detektGradlePluginVersion,
|
||||||
|
"org.cqfn.diktat.diktat-gradle-plugin" to diktatGradlePluginVersion,
|
||||||
)
|
)
|
||||||
|
|
||||||
eachPlugin {
|
eachPlugin {
|
||||||
|
|
@ -35,6 +39,7 @@ include("common-geo")
|
||||||
include("common-geo-spatial4j-spring")
|
include("common-geo-spatial4j-spring")
|
||||||
include("common-messaging")
|
include("common-messaging")
|
||||||
include("captcha")
|
include("captcha")
|
||||||
|
include("codestyle-archunit")
|
||||||
include("logger")
|
include("logger")
|
||||||
include("logger-spring")
|
include("logger-spring")
|
||||||
include("logger-spring-web")
|
include("logger-spring-web")
|
||||||
|
|
@ -62,5 +67,3 @@ include("geoip-core")
|
||||||
include("user-agent")
|
include("user-agent")
|
||||||
include("smart-migration")
|
include("smart-migration")
|
||||||
include("telegram-bot-spring")
|
include("telegram-bot-spring")
|
||||||
include("spreadsheets")
|
|
||||||
include("spreadsheets-google")
|
|
||||||
|
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
plugins {
|
|
||||||
id("kotlin")
|
|
||||||
id("kotlin-spring")
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
api(project(":spreadsheets"))
|
|
||||||
|
|
||||||
implementation(project(":common"))
|
|
||||||
|
|
||||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
|
|
||||||
|
|
||||||
implementation("org.springframework.boot:spring-boot")
|
|
||||||
|
|
||||||
implementation("com.google.api-client:google-api-client")
|
|
||||||
implementation("com.google.apis:google-api-services-sheets")
|
|
||||||
implementation("com.google.auth:google-auth-library-oauth2-http")
|
|
||||||
|
|
||||||
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
package ru.touchin.spreadsheets
|
|
||||||
|
|
||||||
import org.springframework.context.annotation.Import
|
|
||||||
import ru.touchin.spreadsheets.google.configurations.GoogleSpreadSheetsConfiguration
|
|
||||||
|
|
||||||
@Suppress("unused")
|
|
||||||
@Import(value = [GoogleSpreadSheetsConfiguration::class])
|
|
||||||
annotation class EnableGoogleSpreadSheets
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
package ru.touchin.spreadsheets.google.configurations
|
|
||||||
|
|
||||||
import com.google.api.client.http.javanet.NetHttpTransport
|
|
||||||
import com.google.api.client.json.gson.GsonFactory
|
|
||||||
import com.google.api.services.sheets.v4.Sheets
|
|
||||||
import com.google.api.services.sheets.v4.SheetsScopes
|
|
||||||
import com.google.auth.http.HttpCredentialsAdapter
|
|
||||||
import com.google.auth.oauth2.GoogleCredentials
|
|
||||||
import org.springframework.boot.context.properties.ConfigurationPropertiesScan
|
|
||||||
import org.springframework.context.annotation.Bean
|
|
||||||
import org.springframework.context.annotation.ComponentScan
|
|
||||||
import org.springframework.context.annotation.Configuration
|
|
||||||
import ru.touchin.spreadsheets.google.exceptions.GoogleCredentialsMissingException
|
|
||||||
import ru.touchin.spreadsheets.google.properties.GoogleSpreadsheetsProperties
|
|
||||||
import java.io.File
|
|
||||||
import java.io.InputStream
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
@ComponentScan("ru.touchin.spreadsheets.google")
|
|
||||||
@ConfigurationPropertiesScan("ru.touchin.spreadsheets.google")
|
|
||||||
class GoogleSpreadSheetsConfiguration {
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
fun sheets(googleSheetsProperties: GoogleSpreadsheetsProperties): Sheets {
|
|
||||||
val transport = NetHttpTransport.Builder().build()
|
|
||||||
val jsonFactory = GsonFactory.getDefaultInstance()
|
|
||||||
val initializer = HttpCredentialsAdapter(
|
|
||||||
GoogleCredentials.fromStream(getCredentialsStream(googleSheetsProperties))
|
|
||||||
.createScoped(SheetsScopes.SPREADSHEETS)
|
|
||||||
.let { credentials ->
|
|
||||||
googleSheetsProperties.delegate
|
|
||||||
?.let { user ->
|
|
||||||
credentials.createDelegated(user)
|
|
||||||
}
|
|
||||||
?: credentials
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return Sheets.Builder(transport, jsonFactory, initializer)
|
|
||||||
.setApplicationName(googleSheetsProperties.appName)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getCredentialsStream(googleSheetsProperties: GoogleSpreadsheetsProperties): InputStream {
|
|
||||||
return googleSheetsProperties.credentialsPath
|
|
||||||
?.takeIf(String::isNotBlank)
|
|
||||||
?.let { File(it).inputStream() }
|
|
||||||
?: googleSheetsProperties.credentials?.byteInputStream()
|
|
||||||
?: throw GoogleCredentialsMissingException()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
package ru.touchin.spreadsheets.google.enums
|
|
||||||
|
|
||||||
enum class Dimension(val value: String) {
|
|
||||||
Rows("ROWS"),
|
|
||||||
Columns("COLUMNS");
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
package ru.touchin.spreadsheets.google.enums
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @see <a href="https://developers.google.com/sheets/api/reference/rest/v4/ValueInputOption">Documentation</a>
|
|
||||||
*/
|
|
||||||
enum class ValueInputOption(val code: String) {
|
|
||||||
|
|
||||||
RAW("RAW"),
|
|
||||||
USER_ENTERED("USER_ENTERED"),
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
package ru.touchin.spreadsheets.google.enums
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @see <a href="https://developers.google.com/sheets/api/reference/rest/v4/ValueRenderOption">Documentation</a>
|
|
||||||
*/
|
|
||||||
enum class ValueRenderOption(val code: String) {
|
|
||||||
|
|
||||||
FORMATTED_VALUE("FORMATTED_VALUE"),
|
|
||||||
UNFORMATTED_VALUE("UNFORMATTED_VALUE"),
|
|
||||||
FORMULA("FORMULA"),
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
package ru.touchin.spreadsheets.google.exceptions
|
|
||||||
|
|
||||||
import ru.touchin.common.exceptions.CommonException
|
|
||||||
|
|
||||||
class GoogleCredentialsMissingException : CommonException("Google credentials are not set")
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
package ru.touchin.spreadsheets.google.exceptions
|
|
||||||
|
|
||||||
import ru.touchin.common.exceptions.CommonException
|
|
||||||
import ru.touchin.spreadsheets.services.dto.SpreadsheetSource
|
|
||||||
|
|
||||||
class GoogleSheetMissingException private constructor(
|
|
||||||
val source: SpreadsheetSource,
|
|
||||||
message: String,
|
|
||||||
): CommonException(message) {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun create(source: SpreadsheetSource) = GoogleSheetMissingException(
|
|
||||||
source = source,
|
|
||||||
message = "Missing sheet '${source.sheetId}' in spreadsheet '${source.spreadsheetId}",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
package ru.touchin.spreadsheets.google.exceptions
|
|
||||||
|
|
||||||
import ru.touchin.common.exceptions.CommonException
|
|
||||||
|
|
||||||
class InvalidGoogleSpreadsheetsSourceException(
|
|
||||||
url: String,
|
|
||||||
) : CommonException("Unable to parse source $url")
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
package ru.touchin.spreadsheets.google.properties
|
|
||||||
|
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
|
||||||
import org.springframework.boot.context.properties.ConstructorBinding
|
|
||||||
|
|
||||||
@ConstructorBinding
|
|
||||||
@ConfigurationProperties(prefix = "spreadsheets.google")
|
|
||||||
data class GoogleSpreadsheetsProperties(
|
|
||||||
val appName: String,
|
|
||||||
val credentials: String? = null,
|
|
||||||
val credentialsPath: String? = null,
|
|
||||||
val delegate: String? = null,
|
|
||||||
)
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
package ru.touchin.spreadsheets.google.services
|
|
||||||
|
|
||||||
import org.springframework.stereotype.Service
|
|
||||||
import ru.touchin.spreadsheets.google.exceptions.InvalidGoogleSpreadsheetsSourceException
|
|
||||||
import ru.touchin.spreadsheets.services.SpreadsheetsUrlService
|
|
||||||
import ru.touchin.spreadsheets.services.dto.SpreadsheetSource
|
|
||||||
import java.net.URL
|
|
||||||
|
|
||||||
@Service
|
|
||||||
class GoogleSheetsUrlServiceImpl : SpreadsheetsUrlService {
|
|
||||||
|
|
||||||
override fun parse(url: URL): SpreadsheetSource {
|
|
||||||
return parse(url.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun parse(url: String): SpreadsheetSource {
|
|
||||||
val groups = spreadSheetUrlRegex.find(url)?.groups
|
|
||||||
?: throw InvalidGoogleSpreadsheetsSourceException(url)
|
|
||||||
|
|
||||||
val spreadSheetId = groups["spreadsheetId"]?.value
|
|
||||||
?: throw InvalidGoogleSpreadsheetsSourceException(url)
|
|
||||||
|
|
||||||
val sheetId = groups["sheetId"]?.value?.toInt()
|
|
||||||
?: 0
|
|
||||||
|
|
||||||
return SpreadsheetSource(spreadsheetId = spreadSheetId, sheetId = sheetId)
|
|
||||||
}
|
|
||||||
|
|
||||||
private companion object {
|
|
||||||
|
|
||||||
val spreadSheetUrlRegex = Regex(
|
|
||||||
"https://docs\\.google\\.com/spreadsheets/d/(?<spreadsheetId>[\\da-zA-Z-_]+)/edit(?:#gid=(?<sheetId>\\d+))?",
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
package ru.touchin.spreadsheets.google.services
|
|
||||||
|
|
||||||
import org.springframework.beans.factory.ObjectFactory
|
|
||||||
import org.springframework.stereotype.Service
|
|
||||||
import ru.touchin.spreadsheets.services.SpreadsheetBuilder
|
|
||||||
import ru.touchin.spreadsheets.services.SpreadsheetBuilderFactory
|
|
||||||
|
|
||||||
@Service
|
|
||||||
class GoogleSpreadsheetBuilderFactoryImpl(
|
|
||||||
private val spreadsheetBuilderFactory: ObjectFactory<SpreadsheetBuilder>,
|
|
||||||
) : SpreadsheetBuilderFactory {
|
|
||||||
|
|
||||||
override fun create(): SpreadsheetBuilder {
|
|
||||||
return spreadsheetBuilderFactory.`object`
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
package ru.touchin.spreadsheets.google.services
|
|
||||||
|
|
||||||
import com.google.api.services.sheets.v4.Sheets
|
|
||||||
import org.springframework.stereotype.Component
|
|
||||||
import ru.touchin.spreadsheets.services.SpreadsheetBuilder
|
|
||||||
import ru.touchin.spreadsheets.services.SpreadsheetService
|
|
||||||
import ru.touchin.spreadsheets.services.SpreadsheetsUrlService
|
|
||||||
import java.net.URL
|
|
||||||
|
|
||||||
@Component
|
|
||||||
class GoogleSpreadsheetBuilderImpl(
|
|
||||||
private val spreadsheetsUrlService: SpreadsheetsUrlService,
|
|
||||||
private val sheets: Sheets,
|
|
||||||
) : SpreadsheetBuilder {
|
|
||||||
|
|
||||||
private var sheetId: Int? = null
|
|
||||||
|
|
||||||
private var autoInit: Boolean = true
|
|
||||||
|
|
||||||
override fun setSheetId(sheetId: Int): SpreadsheetBuilder = this.also {
|
|
||||||
it.sheetId = sheetId
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setAutoInit(autoInit: Boolean): SpreadsheetBuilder = this.also {
|
|
||||||
it.autoInit = autoInit
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun build(url: URL): SpreadsheetService {
|
|
||||||
val source = spreadsheetsUrlService.parse(url)
|
|
||||||
val sheetId = this.sheetId ?: source.sheetId
|
|
||||||
|
|
||||||
return GoogleSpreadsheetServiceImpl(source.copy(sheetId = sheetId), sheets)
|
|
||||||
.also {
|
|
||||||
if (autoInit) {
|
|
||||||
it.init()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,244 +0,0 @@
|
||||||
package ru.touchin.spreadsheets.google.services
|
|
||||||
|
|
||||||
import com.google.api.services.sheets.v4.Sheets
|
|
||||||
import com.google.api.services.sheets.v4.model.BatchGetValuesByDataFilterRequest
|
|
||||||
import com.google.api.services.sheets.v4.model.BatchUpdateSpreadsheetRequest
|
|
||||||
import com.google.api.services.sheets.v4.model.CellData
|
|
||||||
import com.google.api.services.sheets.v4.model.DataFilter
|
|
||||||
import com.google.api.services.sheets.v4.model.DimensionRange
|
|
||||||
import com.google.api.services.sheets.v4.model.ExtendedValue
|
|
||||||
import com.google.api.services.sheets.v4.model.GridCoordinate
|
|
||||||
import com.google.api.services.sheets.v4.model.GridRange
|
|
||||||
import com.google.api.services.sheets.v4.model.InsertDimensionRequest
|
|
||||||
import com.google.api.services.sheets.v4.model.Request
|
|
||||||
import com.google.api.services.sheets.v4.model.RowData
|
|
||||||
import com.google.api.services.sheets.v4.model.UpdateCellsRequest
|
|
||||||
import ru.touchin.spreadsheets.google.enums.Dimension
|
|
||||||
import ru.touchin.spreadsheets.google.enums.ValueRenderOption
|
|
||||||
import ru.touchin.spreadsheets.google.exceptions.GoogleSheetMissingException
|
|
||||||
import ru.touchin.spreadsheets.services.SpreadsheetService
|
|
||||||
import ru.touchin.spreadsheets.services.dto.CellType
|
|
||||||
import ru.touchin.spreadsheets.services.dto.SheetCell
|
|
||||||
import ru.touchin.spreadsheets.services.dto.SheetHeader
|
|
||||||
import ru.touchin.spreadsheets.services.dto.SheetRange
|
|
||||||
import ru.touchin.spreadsheets.services.dto.Spreadsheet
|
|
||||||
import ru.touchin.spreadsheets.services.dto.SpreadsheetSource
|
|
||||||
import ru.touchin.spreadsheets.services.dto.cells.SheetStringCell
|
|
||||||
import java.math.BigDecimal
|
|
||||||
import kotlin.math.max
|
|
||||||
|
|
||||||
internal class GoogleSpreadsheetServiceImpl(
|
|
||||||
private val source: SpreadsheetSource,
|
|
||||||
private val sheets: Sheets,
|
|
||||||
) : SpreadsheetService {
|
|
||||||
|
|
||||||
private lateinit var spreadsheet: Spreadsheet
|
|
||||||
|
|
||||||
override fun init(): Spreadsheet {
|
|
||||||
val spreadsheet = sheets.spreadsheets()
|
|
||||||
.get(source.spreadsheetId)
|
|
||||||
.execute()
|
|
||||||
|
|
||||||
val sheet = spreadsheet.sheets
|
|
||||||
.find { it.properties.sheetId == source.sheetId }
|
|
||||||
?: throw GoogleSheetMissingException.create(source)
|
|
||||||
|
|
||||||
val gridProperties = sheet.properties.gridProperties
|
|
||||||
|
|
||||||
return Spreadsheet(
|
|
||||||
rowCount = gridProperties.rowCount,
|
|
||||||
columnCount = gridProperties.columnCount,
|
|
||||||
locale = spreadsheet.properties.locale,
|
|
||||||
).also {
|
|
||||||
this.spreadsheet = it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getSheet(): Spreadsheet {
|
|
||||||
return spreadsheet.copy()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getHeader(): SheetHeader {
|
|
||||||
val values = getValues(source.headerRange)
|
|
||||||
.map { rows ->
|
|
||||||
rows.map { SheetStringCell(it.toString()) }
|
|
||||||
}
|
|
||||||
|
|
||||||
return SheetHeader(values)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createHeader(header: SheetHeader) {
|
|
||||||
updateValues(source.headerRange, header.values)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getContentColumn(column: Int): List<Any> {
|
|
||||||
return getValues(
|
|
||||||
getContentRange().copy(
|
|
||||||
startColumnIndex = column,
|
|
||||||
endColumnIndex = column + 1,
|
|
||||||
),
|
|
||||||
dimension = Dimension.Columns,
|
|
||||||
)
|
|
||||||
.firstOrNull()
|
|
||||||
?: emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun updateRows(range: SheetRange, values: List<List<SheetCell>>) {
|
|
||||||
return updateValues(range, values)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun updateContentRows(startRowIndex: Int, values: List<List<SheetCell>>) {
|
|
||||||
updateRows(
|
|
||||||
range = SheetRange(
|
|
||||||
startColumnIndex = 0,
|
|
||||||
startRowIndex = startRowIndex + (source.headerRange.endRowIndex ?: 0),
|
|
||||||
),
|
|
||||||
values = values,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateValues(range: SheetRange, values: List<List<SheetCell>>) {
|
|
||||||
val maxColumnIndex = getMaxIndex(values, Dimension.Columns) + range.startColumnIndex
|
|
||||||
val maxRowIndex = getMaxIndex(values, Dimension.Rows) + range.startRowIndex
|
|
||||||
|
|
||||||
val requests = mutableListOf<Request>()
|
|
||||||
|
|
||||||
if (maxColumnIndex > spreadsheet.columnCount) {
|
|
||||||
requests.add(
|
|
||||||
createInsertRequest(
|
|
||||||
insertCount = maxColumnIndex - spreadsheet.columnCount,
|
|
||||||
dimension = Dimension.Columns,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (maxRowIndex > spreadsheet.rowCount) {
|
|
||||||
requests.add(
|
|
||||||
createInsertRequest(
|
|
||||||
insertCount = maxRowIndex - spreadsheet.rowCount,
|
|
||||||
dimension = Dimension.Rows,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
requests.add(
|
|
||||||
createUpdateCellsRequest(range, values)
|
|
||||||
)
|
|
||||||
|
|
||||||
sheets.spreadsheets()
|
|
||||||
.batchUpdate(
|
|
||||||
source.spreadsheetId,
|
|
||||||
BatchUpdateSpreadsheetRequest().apply {
|
|
||||||
this.requests = requests
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.execute()
|
|
||||||
|
|
||||||
spreadsheet = spreadsheet.copy(
|
|
||||||
columnCount = max(spreadsheet.columnCount, maxColumnIndex),
|
|
||||||
rowCount = max(spreadsheet.rowCount, maxRowIndex),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createUpdateCellsRequest(range: SheetRange, values: List<List<SheetCell>>): Request {
|
|
||||||
return Request().setUpdateCells(
|
|
||||||
UpdateCellsRequest().apply {
|
|
||||||
fields = "*"
|
|
||||||
start = GridCoordinate().apply {
|
|
||||||
sheetId = source.sheetId
|
|
||||||
columnIndex = range.startColumnIndex
|
|
||||||
rowIndex = range.startRowIndex
|
|
||||||
}
|
|
||||||
rows = values.map { rows ->
|
|
||||||
RowData().setValues(
|
|
||||||
rows.map { row ->
|
|
||||||
row.toCellData()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getContentRange(): SheetRange {
|
|
||||||
return SheetRange(
|
|
||||||
startColumnIndex = 0,
|
|
||||||
startRowIndex = source.headerRange.endRowIndex ?: 0,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun SheetCell.toCellData(): CellData {
|
|
||||||
val value = ExtendedValue()
|
|
||||||
|
|
||||||
when(type) {
|
|
||||||
CellType.String -> value.stringValue = this.value.toString()
|
|
||||||
CellType.Null -> value.stringValue = null
|
|
||||||
CellType.Date -> value.stringValue = this.value.toString()
|
|
||||||
CellType.DateTime -> value.stringValue = this.value.toString()
|
|
||||||
CellType.Price -> value.numberValue = (this.value as BigDecimal).toDouble()
|
|
||||||
CellType.Number -> value.numberValue = (this.value as Number).toDouble()
|
|
||||||
}
|
|
||||||
|
|
||||||
return CellData().also { it.userEnteredValue = value }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createInsertRequest(insertCount: Int, dimension: Dimension): Request {
|
|
||||||
val startIndex = when(dimension) {
|
|
||||||
Dimension.Rows -> spreadsheet.rowCount - 1
|
|
||||||
Dimension.Columns -> spreadsheet.columnCount - 1
|
|
||||||
}
|
|
||||||
|
|
||||||
return Request().setInsertDimension(
|
|
||||||
InsertDimensionRequest()
|
|
||||||
.setRange(
|
|
||||||
DimensionRange()
|
|
||||||
.setSheetId(source.sheetId)
|
|
||||||
.setDimension(dimension.value)
|
|
||||||
.setStartIndex(startIndex)
|
|
||||||
.setEndIndex(startIndex + insertCount)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getMaxIndex(values: List<List<SheetCell>>, dimension: Dimension): Int {
|
|
||||||
return when (dimension) {
|
|
||||||
Dimension.Rows -> values.size
|
|
||||||
Dimension.Columns -> values.maxOf { it.size }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getValues(range: SheetRange, dimension: Dimension = Dimension.Rows): List<List<Any>> {
|
|
||||||
val values = sheets.spreadsheets()
|
|
||||||
.values()
|
|
||||||
.batchGetByDataFilter(
|
|
||||||
source.spreadsheetId,
|
|
||||||
BatchGetValuesByDataFilterRequest().apply {
|
|
||||||
majorDimension = dimension.value
|
|
||||||
valueRenderOption = ValueRenderOption.FORMATTED_VALUE.code
|
|
||||||
dataFilters = listOf(
|
|
||||||
DataFilter().apply {
|
|
||||||
gridRange = range.toGridRange()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.execute()
|
|
||||||
.valueRanges
|
|
||||||
.first()
|
|
||||||
.valueRange
|
|
||||||
.getValues()
|
|
||||||
|
|
||||||
return values?.toList() ?: emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun SheetRange.toGridRange(): GridRange {
|
|
||||||
return GridRange().also {
|
|
||||||
it.sheetId = source.sheetId
|
|
||||||
it.startColumnIndex = startColumnIndex
|
|
||||||
it.startRowIndex = startRowIndex
|
|
||||||
it.endColumnIndex = endColumnIndex
|
|
||||||
it.endRowIndex = endRowIndex
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
package ru.touchin.spreadsheets.google.utils
|
|
||||||
|
|
||||||
import java.net.URL
|
|
||||||
|
|
||||||
object GoogleSpreadSheetsFormulaParserUtil {
|
|
||||||
|
|
||||||
private const val HYPERLINK_REGEX_LINK_INDEX: Int = 1
|
|
||||||
private const val RUS_LOCALE = "ru_RU"
|
|
||||||
private const val DEFAULT_DIVIDER = ","
|
|
||||||
private const val RUS_DIVIDER = ";"
|
|
||||||
|
|
||||||
fun isFormulaHyperlink(jiraIssueLink: String, locale: String): Boolean {
|
|
||||||
return getHyperLinkRegexByLocale(locale).matches(jiraIssueLink)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun asFormulaHyperlink(url: URL, label: String, locale: String): String {
|
|
||||||
val divider = getFormulaeDividerByLocale(locale)
|
|
||||||
return "=HYPERLINK(\"$url\"$divider\"$label\")"
|
|
||||||
}
|
|
||||||
|
|
||||||
fun fromFormulaHyperlink(hyperlink: String, locale: String): URL {
|
|
||||||
return getHyperLinkRegexByLocale(locale).matchEntire(hyperlink)
|
|
||||||
?.groupValues
|
|
||||||
?.get(HYPERLINK_REGEX_LINK_INDEX)
|
|
||||||
?.trim { it == '"' }
|
|
||||||
?.let { URL(it) }
|
|
||||||
?: throw IllegalStateException("Could not parse hyperlink \"$hyperlink\"")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getHyperLinkRegexByLocale(locale: String): Regex {
|
|
||||||
val divider = getFormulaeDividerByLocale(locale)
|
|
||||||
return Regex("=HYPERLINK\\((.*)$divider(.*)\\)")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getFormulaeDividerByLocale(locale: String): String {
|
|
||||||
return if (locale == RUS_LOCALE) {
|
|
||||||
RUS_DIVIDER
|
|
||||||
} else {
|
|
||||||
DEFAULT_DIVIDER
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
package ru.touchin.sheets.google
|
|
||||||
|
|
||||||
import org.springframework.boot.SpringBootConfiguration
|
|
||||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
|
|
||||||
import org.springframework.boot.test.context.TestConfiguration
|
|
||||||
import org.springframework.context.annotation.ComponentScan
|
|
||||||
|
|
||||||
@ComponentScan
|
|
||||||
@TestConfiguration
|
|
||||||
@SpringBootConfiguration
|
|
||||||
@EnableAutoConfiguration
|
|
||||||
class GoogleSheetsTestApplication
|
|
||||||
|
|
@ -1,96 +0,0 @@
|
||||||
package ru.touchin.sheets.google.services
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.Assertions.*
|
|
||||||
import org.junit.jupiter.api.Test
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
|
||||||
import org.springframework.boot.test.context.SpringBootTest
|
|
||||||
import ru.touchin.spreadsheets.google.exceptions.InvalidGoogleSpreadsheetsSourceException
|
|
||||||
import ru.touchin.spreadsheets.google.services.GoogleSheetsUrlServiceImpl
|
|
||||||
import ru.touchin.spreadsheets.services.SpreadsheetsUrlService
|
|
||||||
import ru.touchin.spreadsheets.services.dto.SpreadsheetSource
|
|
||||||
|
|
||||||
@SpringBootTest(
|
|
||||||
classes = [
|
|
||||||
GoogleSheetsUrlServiceImpl::class,
|
|
||||||
]
|
|
||||||
)
|
|
||||||
internal class GoogleSheetsUrlServiceImplTest {
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
lateinit var sheetsUrlService: SpreadsheetsUrlService
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun shouldParseOk() {
|
|
||||||
val expectedSpreadsheetSource = SpreadsheetSource(
|
|
||||||
spreadsheetId = "1KgQTBmFq-yddGotA5PUd5SRkg-J3dtdULpRNz7uctAE",
|
|
||||||
sheetId = 1,
|
|
||||||
)
|
|
||||||
|
|
||||||
val url = "https://docs.google.com/spreadsheets/d/${expectedSpreadsheetSource.spreadsheetId}/edit#gid=${expectedSpreadsheetSource.sheetId}"
|
|
||||||
|
|
||||||
val text1 = """
|
|
||||||
Test
|
|
||||||
Test $url
|
|
||||||
ssss
|
|
||||||
""".trimIndent()
|
|
||||||
|
|
||||||
val actualSpreadsheetSource1 = sheetsUrlService.parse(text1)
|
|
||||||
|
|
||||||
assertEquals(expectedSpreadsheetSource, actualSpreadsheetSource1)
|
|
||||||
|
|
||||||
val text2 = """
|
|
||||||
Test ($url)
|
|
||||||
""".trimIndent()
|
|
||||||
|
|
||||||
val actualSpreadsheetSource2 = sheetsUrlService.parse(text2)
|
|
||||||
|
|
||||||
assertEquals(expectedSpreadsheetSource, actualSpreadsheetSource2)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun shouldParseOk2() {
|
|
||||||
val expectedSpreadsheetSource = SpreadsheetSource(
|
|
||||||
spreadsheetId = "1KgQTBmFq-yddGotA5PUd5SRkg-J3dtdULpRNz7uctAE",
|
|
||||||
sheetId = 0,
|
|
||||||
)
|
|
||||||
|
|
||||||
val text = """
|
|
||||||
Test
|
|
||||||
Test https://docs.google.com/spreadsheets/d/${expectedSpreadsheetSource.spreadsheetId}/edit Test
|
|
||||||
Test
|
|
||||||
""".trimIndent()
|
|
||||||
|
|
||||||
val actualSpreadsheetSource1 = sheetsUrlService.parse(text)
|
|
||||||
|
|
||||||
assertEquals(expectedSpreadsheetSource, actualSpreadsheetSource1)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun shouldThrowException() {
|
|
||||||
val text1 = """
|
|
||||||
Test
|
|
||||||
""".trimIndent()
|
|
||||||
|
|
||||||
assertThrows(InvalidGoogleSpreadsheetsSourceException::class.java) {
|
|
||||||
sheetsUrlService.parse(text1)
|
|
||||||
}
|
|
||||||
|
|
||||||
val text2 = """
|
|
||||||
Test (https://dcs.google.com/spreadsheets/d/1KgQTBmFq-yddGotA5PUd5SRkg-J3dtdULpRNz7uctAE/edit#gid=0)
|
|
||||||
""".trimIndent()
|
|
||||||
|
|
||||||
assertThrows(InvalidGoogleSpreadsheetsSourceException::class.java) {
|
|
||||||
sheetsUrlService.parse(text2)
|
|
||||||
}
|
|
||||||
|
|
||||||
val text3 = """
|
|
||||||
Test (https://dcs.google.com/spreadsheets/d/1KgQTBmFq-yddGotA5PUd5SRkg-J
|
|
||||||
3dtdULpRNz7uctAE/edit#gid=0)
|
|
||||||
""".trimIndent()
|
|
||||||
|
|
||||||
assertThrows(InvalidGoogleSpreadsheetsSourceException::class.java) {
|
|
||||||
sheetsUrlService.parse(text3)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
package ru.touchin.sheets.google.utils
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.Assertions
|
|
||||||
import org.junit.jupiter.api.Test
|
|
||||||
import ru.touchin.spreadsheets.google.utils.GoogleSpreadSheetsFormulaParserUtil
|
|
||||||
import java.net.URL
|
|
||||||
|
|
||||||
class GoogleSheetsFormulaParserUtilTest {
|
|
||||||
|
|
||||||
private val url = URL("https://example.com")
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun isFormulaHyperlink_trueIfCorrectSyntax() {
|
|
||||||
val text = "=HYPERLINK(someurl;label)"
|
|
||||||
|
|
||||||
val result = GoogleSpreadSheetsFormulaParserUtil.isFormulaHyperlink(text, "ru_RU")
|
|
||||||
|
|
||||||
Assertions.assertTrue(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun isFormulaHyperlink_falseIfUrl() {
|
|
||||||
val result = GoogleSpreadSheetsFormulaParserUtil.isFormulaHyperlink(url.toString(), "ru_RU")
|
|
||||||
|
|
||||||
Assertions.assertFalse(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun asFormulaHyperlink_correct() {
|
|
||||||
val label = "link"
|
|
||||||
|
|
||||||
val expected = "=HYPERLINK(\"$url\";\"$label\")"
|
|
||||||
val actual = GoogleSpreadSheetsFormulaParserUtil.asFormulaHyperlink(url, label, "ru_RU")
|
|
||||||
|
|
||||||
Assertions.assertEquals(
|
|
||||||
expected,
|
|
||||||
actual
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun fromFormulaHyperlink_correct() {
|
|
||||||
val expected = url.toString()
|
|
||||||
val actual = GoogleSpreadSheetsFormulaParserUtil.fromFormulaHyperlink("=HYPERLINK(\"$url\";\"label\")", "ru_RU").toString()
|
|
||||||
|
|
||||||
|
|
||||||
Assertions.assertEquals(
|
|
||||||
expected,
|
|
||||||
actual
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
plugins {
|
|
||||||
id("kotlin")
|
|
||||||
id("kotlin-spring")
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
package ru.touchin.spreadsheets.services
|
|
||||||
|
|
||||||
import java.net.URL
|
|
||||||
|
|
||||||
interface SpreadsheetBuilder {
|
|
||||||
|
|
||||||
fun setSheetId(sheetId: Int): SpreadsheetBuilder
|
|
||||||
fun setAutoInit(autoInit: Boolean): SpreadsheetBuilder
|
|
||||||
|
|
||||||
fun build(url: URL): SpreadsheetService
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
package ru.touchin.spreadsheets.services
|
|
||||||
|
|
||||||
interface SpreadsheetBuilderFactory {
|
|
||||||
|
|
||||||
fun create(): SpreadsheetBuilder
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
package ru.touchin.spreadsheets.services
|
|
||||||
|
|
||||||
import ru.touchin.spreadsheets.services.dto.SheetCell
|
|
||||||
import ru.touchin.spreadsheets.services.dto.SheetHeader
|
|
||||||
import ru.touchin.spreadsheets.services.dto.SheetRange
|
|
||||||
import ru.touchin.spreadsheets.services.dto.Spreadsheet
|
|
||||||
|
|
||||||
interface SpreadsheetService {
|
|
||||||
|
|
||||||
fun init(): Spreadsheet
|
|
||||||
|
|
||||||
fun getSheet(): Spreadsheet
|
|
||||||
|
|
||||||
fun getHeader(): SheetHeader
|
|
||||||
|
|
||||||
fun createHeader(header: SheetHeader)
|
|
||||||
|
|
||||||
fun getContentColumn(column: Int): List<Any>
|
|
||||||
|
|
||||||
fun updateRows(range: SheetRange, values: List<List<SheetCell>>)
|
|
||||||
|
|
||||||
fun updateContentRows(startRowIndex: Int, values: List<List<SheetCell>>)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
package ru.touchin.spreadsheets.services
|
|
||||||
|
|
||||||
import ru.touchin.spreadsheets.services.dto.SpreadsheetSource
|
|
||||||
import java.net.URL
|
|
||||||
|
|
||||||
interface SpreadsheetsUrlService {
|
|
||||||
|
|
||||||
fun parse(url: URL): SpreadsheetSource
|
|
||||||
|
|
||||||
fun parse(url: String): SpreadsheetSource
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
package ru.touchin.spreadsheets.services.dto
|
|
||||||
|
|
||||||
enum class CellType {
|
|
||||||
Null,
|
|
||||||
String,
|
|
||||||
Date,
|
|
||||||
DateTime,
|
|
||||||
Price,
|
|
||||||
Number,
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
package ru.touchin.spreadsheets.services.dto
|
|
||||||
|
|
||||||
interface SheetCell {
|
|
||||||
val value: Any
|
|
||||||
val type: CellType
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
package ru.touchin.spreadsheets.services.dto
|
|
||||||
|
|
||||||
data class SheetHeader(
|
|
||||||
val values: List<List<SheetCell>>
|
|
||||||
) {
|
|
||||||
|
|
||||||
fun hasHeader() = values.isNotEmpty()
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
package ru.touchin.spreadsheets.services.dto
|
|
||||||
|
|
||||||
data class SheetRange(
|
|
||||||
val startColumnIndex: Int,
|
|
||||||
val startRowIndex: Int,
|
|
||||||
val endColumnIndex: Int? = null,
|
|
||||||
val endRowIndex: Int? = null,
|
|
||||||
)
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
package ru.touchin.spreadsheets.services.dto
|
|
||||||
|
|
||||||
data class Spreadsheet(
|
|
||||||
val rowCount: Int,
|
|
||||||
val columnCount: Int,
|
|
||||||
val locale: String,
|
|
||||||
)
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
package ru.touchin.spreadsheets.services.dto
|
|
||||||
|
|
||||||
data class SpreadsheetSource(
|
|
||||||
val spreadsheetId: String,
|
|
||||||
val sheetId: Int,
|
|
||||||
val headerRange: SheetRange = DEFAULT_HEADER_RANGE,
|
|
||||||
) {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val DEFAULT_HEADER_RANGE = SheetRange(
|
|
||||||
startColumnIndex = 0,
|
|
||||||
startRowIndex = 0,
|
|
||||||
endRowIndex = 1,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
package ru.touchin.spreadsheets.services.dto.cells
|
|
||||||
|
|
||||||
import ru.touchin.spreadsheets.services.dto.CellType
|
|
||||||
import ru.touchin.spreadsheets.services.dto.SheetCell
|
|
||||||
|
|
||||||
class SheetNullCell() : SheetCell {
|
|
||||||
|
|
||||||
override val value = 0
|
|
||||||
|
|
||||||
override val type = CellType.Null
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (javaClass != other?.javaClass) return false
|
|
||||||
|
|
||||||
other as SheetNullCell
|
|
||||||
|
|
||||||
if (value != other.value) return false
|
|
||||||
if (type != other.type) return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
var result = value
|
|
||||||
result = 31 * result + type.hashCode()
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
package ru.touchin.spreadsheets.services.dto.cells
|
|
||||||
|
|
||||||
import ru.touchin.spreadsheets.services.dto.CellType
|
|
||||||
import ru.touchin.spreadsheets.services.dto.SheetCell
|
|
||||||
|
|
||||||
data class SheetNumberCell(override val value: Number) : SheetCell {
|
|
||||||
|
|
||||||
override val type = CellType.Number
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
package ru.touchin.spreadsheets.services.dto.cells
|
|
||||||
|
|
||||||
import ru.touchin.spreadsheets.services.dto.CellType
|
|
||||||
import ru.touchin.spreadsheets.services.dto.SheetCell
|
|
||||||
|
|
||||||
data class SheetStringCell(override val value: String) : SheetCell {
|
|
||||||
|
|
||||||
override val type = CellType.String
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -4,7 +4,7 @@ plugins {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api 'org.telegram:telegrambots-spring-boot-starter'
|
api 'org.telegram:telegrambots-spring-boot-starter:6.3.0'
|
||||||
|
|
||||||
implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8'
|
implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8'
|
||||||
implementation 'com.fasterxml.jackson.core:jackson-databind'
|
implementation 'com.fasterxml.jackson.core:jackson-databind'
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue