Compare commits

...

7 Commits

Author SHA1 Message Date
Korna e1a10dd039 Add configuration and properties 2022-11-02 16:12:33 +03:00
Korna 7f8c17a938 Add conditional webclient 2022-11-02 16:12:02 +03:00
Korna a7146ad28f Add api enum 2022-11-02 16:11:45 +03:00
Korna 6617d1c5c0 Add builder 2022-11-02 16:11:36 +03:00
Korna 8289858818 Add module 2022-11-02 16:11:23 +03:00
Korna 2a557de252 Add new platform and provider 2022-11-02 16:08:49 +03:00
Korna b33082e1fe Rename and add dto's 2022-11-02 16:08:26 +03:00
26 changed files with 370 additions and 54 deletions

View File

@ -6,7 +6,7 @@ import org.springframework.stereotype.Component
import ru.touchin.push.message.provider.dto.request.PushTokenCheck
import ru.touchin.push.message.provider.dto.request.PushTokenMessage
import ru.touchin.push.message.provider.dto.result.SendPushResult
import ru.touchin.push.message.provider.dto.result.SendPushTokenMessageResult
import ru.touchin.push.message.provider.dto.result.SendPushTokenMessageTraceableResult
import ru.touchin.push.message.provider.enums.PushTokenStatus
import ru.touchin.push.message.provider.exceptions.InvalidPushTokenException
import ru.touchin.push.message.provider.exceptions.PushMessageProviderException
@ -32,7 +32,7 @@ class FcmClient(
fun check(request: PushTokenCheck): PushTokenStatus {
val validationRequest = PushTokenMessage(
token = request.pushToken,
notification = null,
pushMessageNotification = null,
data = emptyMap()
)
@ -54,7 +54,7 @@ class FcmClient(
return try {
val messageId = firebaseMessaging.send(message, dryRun)
SendPushTokenMessageResult(messageId)
SendPushTokenMessageTraceableResult(messageId)
} catch (e: FirebaseMessagingException) {
throw firebaseMessagingExceptionConverter(e)
}

View File

@ -1,18 +0,0 @@
package ru.touchin.push.message.provider.fcm.converters
import com.google.firebase.messaging.Notification as FcmNotification
import org.springframework.stereotype.Component
import ru.touchin.push.message.provider.dto.Notification
@Component
class NotificationConverter {
operator fun invoke(notification: Notification): FcmNotification {
return FcmNotification.builder()
.setTitle(notification.title)
.setBody(notification.description)
.setImage(notification.imageUrl)
.build()
}
}

View File

@ -0,0 +1,18 @@
package ru.touchin.push.message.provider.fcm.converters
import com.google.firebase.messaging.Notification as FcmNotification
import org.springframework.stereotype.Component
import ru.touchin.push.message.provider.dto.PushMessageNotification
@Component
class PushMessageNotificationConverter {
operator fun invoke(pushMessageNotification: PushMessageNotification): FcmNotification {
return FcmNotification.builder()
.setTitle(pushMessageNotification.title)
.setBody(pushMessageNotification.description)
.setImage(pushMessageNotification.imageUrl)
.build()
}
}

View File

@ -6,12 +6,12 @@ import com.google.firebase.messaging.ApnsConfig
import com.google.firebase.messaging.Aps
import com.google.firebase.messaging.Message
import org.springframework.stereotype.Component
import ru.touchin.push.message.provider.dto.Notification
import ru.touchin.push.message.provider.dto.PushMessageNotification
import ru.touchin.push.message.provider.dto.request.PushTokenMessage
@Component
class PushTokenMessageConverter(
private val notificationConverter: NotificationConverter
private val pushMessageNotificationConverter: PushMessageNotificationConverter
) {
private companion object {
@ -26,14 +26,14 @@ class PushTokenMessageConverter(
.setToken(request.token)
.setupApns()
.setupAndroid()
.setIfExists(request.notification)
.setIfExists(request.pushMessageNotification)
.putAllData(request.data)
.build()
}
private fun Message.Builder.setIfExists(notification: Notification?): Message.Builder {
return if (notification != null) {
setNotification(notificationConverter(notification))
private fun Message.Builder.setIfExists(pushMessageNotification: PushMessageNotification?): Message.Builder {
return if (pushMessageNotification != null) {
setNotification(pushMessageNotificationConverter(pushMessageNotification))
} else {
this
}

View File

@ -5,15 +5,15 @@ import org.junit.Assert
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import ru.touchin.push.message.provider.dto.Notification
import ru.touchin.push.message.provider.dto.PushMessageNotification
import com.google.firebase.messaging.Notification as FcmNotification
import org.junit.jupiter.api.DisplayName
@SpringBootTest
class NotificationConverterTest {
class PushMessageNotificationConverterTest {
@Autowired
lateinit var notificationConverter: NotificationConverter
lateinit var pushMessageNotificationConverter: PushMessageNotificationConverter
@Autowired
lateinit var objectMapper: ObjectMapper
@ -21,19 +21,19 @@ class NotificationConverterTest {
@Test
@DisplayName("Конвертация уведомления происходит корректно")
fun invoke_basic() {
val notification = Notification(
val pushMessageNotification = PushMessageNotification(
title = "title",
description = "description",
imageUrl = "imageUrl"
)
val realResult = notificationConverter(notification)
val realResult = pushMessageNotificationConverter(pushMessageNotification)
val realResultJson = objectMapper.writeValueAsString(realResult)
val expectedResult = FcmNotification.builder()
.setTitle(notification.title)
.setBody(notification.description)
.setImage(notification.imageUrl)
.setTitle(pushMessageNotification.title)
.setBody(pushMessageNotification.description)
.setImage(pushMessageNotification.imageUrl)
.build()
val expectedResultJson = objectMapper.writeValueAsString(expectedResult)

View File

@ -11,7 +11,7 @@ import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import ru.touchin.push.message.provider.dto.Notification
import ru.touchin.push.message.provider.dto.PushMessageNotification
import ru.touchin.push.message.provider.dto.request.PushTokenMessage
@SpringBootTest
@ -21,7 +21,7 @@ class PushTokenMessageConverterTest {
lateinit var pushTokenMessageConverter: PushTokenMessageConverter
@Autowired
lateinit var notificationConverter: NotificationConverter
lateinit var pushMessageNotificationConverter: PushMessageNotificationConverter
@Autowired
lateinit var objectMapper: ObjectMapper
@ -29,14 +29,14 @@ class PushTokenMessageConverterTest {
@Test
@DisplayName("Конвертация сообщения с уведомлением происходит корректно")
fun invoke_withNotification() {
val notification = Notification(
val pushMessageNotification = PushMessageNotification(
title = "title",
description = "description",
imageUrl = "imageUrl"
)
val pushTokenMessage = PushTokenMessage(
token = "token",
notification = notification,
pushMessageNotification = pushMessageNotification,
data = mapOf("testKey" to "testvalue")
)
@ -45,7 +45,7 @@ class PushTokenMessageConverterTest {
val expectedResult = Message.builder()
.setToken(pushTokenMessage.token)
.setNotification(notificationConverter(notification))
.setNotification(pushMessageNotificationConverter(pushMessageNotification))
.putAllData(pushTokenMessage.data)
.setupApns()
.setupAndroid()
@ -65,7 +65,7 @@ class PushTokenMessageConverterTest {
fun invoke_withoutNotification() {
val pushTokenMessage = PushTokenMessage(
token = "token",
notification = null,
pushMessageNotification = null,
data = mapOf("testKey" to "testvalue")
)

View File

@ -10,7 +10,7 @@ import org.springframework.boot.test.mock.mockito.MockBean
import ru.touchin.push.message.provider.dto.request.PushTokenCheck
import ru.touchin.push.message.provider.dto.request.PushTokenMessage
import ru.touchin.push.message.provider.dto.result.CheckPushTokenResult
import ru.touchin.push.message.provider.dto.result.SendPushTokenMessageResult
import ru.touchin.push.message.provider.dto.result.SendPushTokenMessageTraceableResult
import ru.touchin.push.message.provider.enums.PushTokenStatus
import ru.touchin.push.message.provider.fcm.clients.FcmClient
import ru.touchin.push.message.provider.services.PushMessageProviderService
@ -29,11 +29,11 @@ class PushMessageProviderFcmServiceTest {
fun send_basic() {
val request = PushTokenMessage(
token = "testToken",
notification = null,
pushMessageNotification = null,
data = emptyMap()
)
val expectedResult = SendPushTokenMessageResult("testMessageId")
val expectedResult = SendPushTokenMessageTraceableResult("testMessageId")
Mockito.`when`(
fcmClient.sendPushTokenMessage(request)

View File

@ -0,0 +1,21 @@
plugins {
id("kotlin")
id("kotlin-spring")
id("maven-publish")
}
dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation(project(":logger-spring"))
implementation(project(":common-spring-web"))
implementation(project(":push-message-provider"))
testImplementation(project(":logger-spring-web"))
testImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.testcontainers:junit-jupiter")
}

View File

@ -0,0 +1,7 @@
package ru.touchin.push.message.provider.hpk
import org.springframework.context.annotation.Import
import ru.touchin.push.message.provider.hpk.configurations.PushMessageProviderHpkConfiguration
@Import(value = [PushMessageProviderHpkConfiguration::class])
annotation class EnablePushMessageProviderHpk

View File

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

View File

@ -0,0 +1,42 @@
package ru.touchin.push.message.provider.hpk.base.clients
import com.fasterxml.jackson.databind.ObjectMapper
import org.springframework.web.reactive.function.client.ClientResponse
import ru.touchin.push.message.provider.hpk.base.clients.dto.ConditionalResponse
internal open class ConditionalWebClientParser(
private val objectMapper: ObjectMapper,
) {
open fun isOkResponse(clientResponse: ClientResponse): Boolean {
return clientResponse.statusCode().is2xxSuccessful
}
@Throws(Exception::class)
inline fun <reified S, reified F> parse(
clientResponse: ClientResponse,
body: String,
): ConditionalResponse<S, F> {
return if (isOkResponse(clientResponse)) {
ConditionalResponse<S, F>(
success = parseValue(body, S::class.java),
failure = null
)
} else {
ConditionalResponse(
success = null,
failure = parseValue(body, F::class.java)
)
}
}
private fun <T> parseValue(source: String?, clazz: Class<T>): T {
return if (clazz.canonicalName != String::class.java.canonicalName) {
objectMapper.readValue(source, clazz)
} else {
@Suppress("UNCHECKED_CAST")
source as T // T is String
}
}
}

View File

@ -0,0 +1,85 @@
package ru.touchin.push.message.provider.hpk.base.clients
import io.netty.channel.ChannelOption
import io.netty.handler.ssl.SslContextBuilder
import io.netty.handler.timeout.ReadTimeoutHandler
import io.netty.handler.timeout.WriteTimeoutHandler
import org.springframework.http.client.reactive.ReactorClientHttpConnector
import org.springframework.web.reactive.function.client.ClientResponse
import org.springframework.web.reactive.function.client.WebClient
import reactor.core.publisher.Mono
import reactor.core.scheduler.Schedulers
import reactor.netty.http.client.HttpClient
import ru.touchin.common.spring.web.webclient.BaseLogWebClient
import ru.touchin.common.spring.web.webclient.dto.RequestLogData
import ru.touchin.common.spring.web.webclient.logger.WebClientLogger
import ru.touchin.push.message.provider.hpk.base.clients.dto.ConditionalResponse
import ru.touchin.push.message.provider.hpk.properties.HpkProperties
import java.util.concurrent.TimeUnit
abstract class ConfigurableWebClient(
webClientLogger: WebClientLogger,
webClientBuilder: WebClient.Builder,
protected val webService: HpkProperties.WebService,
) : BaseLogWebClient(webClientLogger, webClientBuilder) {
private val conditionalWebClientParser: Lazy<ConditionalWebClientParser> = lazy {
ConditionalWebClientParser(
objectMapper = getObjectMapper(),
)
}
protected fun WebClient.Builder.setTimeouts(): WebClient.Builder {
val httpClient: HttpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, webService.http.connectionTimeout.toMillis().toInt())
.doOnConnected { setup ->
setup.addHandlerLast(ReadTimeoutHandler(webService.http.readTimeout.toMillis(), TimeUnit.MILLISECONDS))
setup.addHandlerLast(WriteTimeoutHandler(webService.http.writeTimeout.toMillis(), TimeUnit.MILLISECONDS))
}
.let { httpClient ->
webService.ssl?.let { ssl ->
httpClient.secure { builder ->
builder
.sslContext(SslContextBuilder.forClient().build())
.handshakeTimeout(ssl.handshakeTimeout)
.closeNotifyFlushTimeout(ssl.notifyFlushTimeout)
.closeNotifyReadTimeout(ssl.notifyReadTimeout)
}
} ?: httpClient
}
return clientConnector(ReactorClientHttpConnector(httpClient))
}
internal inline fun <reified S, reified F> WebClient.RequestHeadersSpec<*>.exchangeWithWrap(
requestLogData: RequestLogData,
): Mono<ConditionalResponse<S, F>> {
return exchangeToMono { clientResponse ->
parse<S, F>(clientResponse)
}.doOnNext { responseWrapper ->
getLogger().log(
requestLogData.copy(
responseBody = responseWrapper.success ?: responseWrapper.failure
)
)
}
}
internal inline fun <reified S, reified F> parse(
clientResponse: ClientResponse,
): Mono<ConditionalResponse<S, F>> {
val responseBody = clientResponse
.bodyToMono(String::class.java)
.defaultIfEmpty(String())
.publishOn(Schedulers.parallel())
return responseBody
.map { body ->
conditionalWebClientParser.value.parse(
clientResponse = clientResponse,
body = body,
)
}
}
}

View File

@ -0,0 +1,15 @@
package ru.touchin.push.message.provider.hpk.base.clients.dto
internal open class ConditionalResponse<S, F>(
val success: S?,
val failure: F?,
) {
init {
// Only one value should be present
check((success == null) != (failure == null))
}
val isSuccess: Boolean = success != null
}

View File

@ -0,0 +1,12 @@
package ru.touchin.push.message.provider.hpk.base.enums
import com.fasterxml.jackson.annotation.JsonValue
internal interface ValueableSerializableEnum<T> {
val value: T
@JsonValue
fun toValue(): T = value
}

View File

@ -0,0 +1,7 @@
package ru.touchin.push.message.provider.hpk.base.extensions
import ru.touchin.push.message.provider.hpk.base.builders.Buildable
fun <V, B : Buildable> B.ifNotNull(value: V?, setter: B.(V) -> B): B {
return value?.let { setter(it) } ?: this
}

View File

@ -0,0 +1,59 @@
package ru.touchin.push.message.provider.hpk.configurations
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.PropertyNamingStrategies
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.context.properties.ConfigurationPropertiesScan
import org.springframework.cache.CacheManager
import org.springframework.cache.concurrent.ConcurrentMapCache
import org.springframework.cache.support.SimpleCacheManager
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.ComponentScan
import org.springframework.context.annotation.Import
import ru.touchin.push.message.provider.configurations.PushMessageProviderConfiguration
import ru.touchin.push.message.provider.hpk.services.HmsOauthAccessTokenCacheServiceImpl.Companion.HMS_CLIENT_SERVICE_CACHE_KEY
@ComponentScan("ru.touchin.push.message.provider.hpk")
@ConfigurationPropertiesScan(basePackages = ["ru.touchin.push.message.provider.hpk"])
@Import(value = [PushMessageProviderConfiguration::class])
class PushMessageProviderHpkConfiguration {
@Bean
@Qualifier("push-message-provider.hpk.webclient-objectmapper")
fun webclientObjectMapper(): ObjectMapper {
return jacksonObjectMapper()
.registerModule(JavaTimeModule())
.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
.setSerializationInclusion(JsonInclude.Include.NON_EMPTY)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
}
@Bean
@Qualifier("push-message-provider.hpk.client-objectmapper")
fun clientObjectMapper(): ObjectMapper {
return jacksonObjectMapper()
.registerModule(JavaTimeModule())
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
.setSerializationInclusion(JsonInclude.Include.NON_EMPTY)
}
@Bean
@ConditionalOnMissingBean
@Qualifier("push-message-provider.hpk.webclient-cachemanager")
fun cacheManager(): CacheManager {
return SimpleCacheManager().also {
it.setCaches(
listOf(
ConcurrentMapCache(HMS_CLIENT_SERVICE_CACHE_KEY)
)
)
}
}
}

View File

@ -0,0 +1,59 @@
package ru.touchin.push.message.provider.hpk.properties
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.ConstructorBinding
import java.net.URL
import java.time.Duration
@ConstructorBinding
@ConfigurationProperties(prefix = "push-message-provider.hpk")
data class HpkProperties(
val webServices: WebServices,
) {
class WebServices(
val clientId: String,
val oauth: Oauth,
val hpk: Hpk,
)
class Oauth(
val clientSecret: String,
url: URL,
http: Http,
ssl: Ssl?,
) : WebService(
url = url,
http = http,
ssl = ssl,
)
class Hpk(
url: URL,
http: Http,
ssl: Ssl?,
) : WebService(
url = url,
http = http,
ssl = ssl,
)
open class WebService(
val url: URL,
val http: Http,
val ssl: Ssl?,
)
class Http(
val readTimeout: Duration,
val writeTimeout: Duration,
val connectionTimeout: Duration,
)
class Ssl(
val handshakeTimeout: Duration,
val notifyFlushTimeout: Duration,
val notifyReadTimeout: Duration,
)
}

View File

@ -1,6 +1,6 @@
package ru.touchin.push.message.provider.dto
class Notification(
class PushMessageNotification(
val title: String?,
val description: String?,
val imageUrl: String?

View File

@ -1,9 +1,9 @@
package ru.touchin.push.message.provider.dto.request
import ru.touchin.push.message.provider.dto.Notification
import ru.touchin.push.message.provider.dto.PushMessageNotification
class PushTokenMessage(
val token: String,
override val notification: Notification?,
override val pushMessageNotification: PushMessageNotification?,
override val data: Map<String, String>
) : SendPushRequest

View File

@ -1,10 +1,10 @@
package ru.touchin.push.message.provider.dto.request
import ru.touchin.push.message.provider.dto.Notification
import ru.touchin.push.message.provider.dto.PushMessageNotification
sealed interface SendPushRequest {
val notification: Notification?
val pushMessageNotification: PushMessageNotification?
val data: Map<String, String>
}

View File

@ -3,5 +3,5 @@ package ru.touchin.push.message.provider.dto.result
import ru.touchin.push.message.provider.enums.PushTokenStatus
data class CheckPushTokenResult(
val status: PushTokenStatus
val status: PushTokenStatus,
)

View File

@ -1,5 +1,3 @@
package ru.touchin.push.message.provider.dto.result
class SendPushTokenMessageResult(
val messageId: String
) : SendPushResult
object SendPushTokenMessageResult : SendPushResult

View File

@ -0,0 +1,5 @@
package ru.touchin.push.message.provider.dto.result
data class SendPushTokenMessageTraceableResult(
val messageId: String
) : SendPushResult

View File

@ -3,6 +3,7 @@ package ru.touchin.push.message.provider.enums
enum class PlatformType {
ANDROID_GOOGLE,
ANDROID_HUAWEI,
IOS
}

View File

@ -2,6 +2,7 @@ package ru.touchin.push.message.provider.enums
enum class PushMessageProviderType {
FCM
FCM,
HPK,
}

View File

@ -45,6 +45,7 @@ include("validation-spring")
include("version-spring-web")
include("push-message-provider")
include("push-message-provider-fcm")
include("push-message-provider-hpk")
include("response-wrapper-spring-web")
include("settings-spring-jpa")
include("security-authorization-server-core")