Init
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
FROM bellsoft/liberica-openjre-alpine:21.0.5
|
||||
|
||||
ENV JAVA_OPTS=""
|
||||
|
||||
ADD ./build/libs/*.jar /app.jar
|
||||
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar /app.jar"]
|
||||
@@ -0,0 +1,103 @@
|
||||
|
||||
group = "space.nstart.pcp"
|
||||
|
||||
plugins {
|
||||
kotlin("jvm")
|
||||
kotlin("plugin.spring")
|
||||
kotlin("plugin.jpa")
|
||||
kotlin("plugin.lombok")
|
||||
id("org.springframework.boot")
|
||||
id("io.spring.dependency-management")
|
||||
id("org.sonarqube")
|
||||
jacoco
|
||||
// id("org.graalvm.buildtools.native")
|
||||
}
|
||||
|
||||
version = "1.0.0"
|
||||
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
freeCompilerArgs.addAll("-Xjsr305=strict")
|
||||
}
|
||||
}
|
||||
|
||||
configurations {
|
||||
compileOnly {
|
||||
extendsFrom(configurations.annotationProcessor.get())
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":libs:pcp-types-lib"))
|
||||
implementation(project(":libs:ballistics-lib"))
|
||||
implementation("${property("dep.spring.actuator")}")
|
||||
implementation("org.springframework.boot:spring-boot-starter-logging")
|
||||
implementation("org.springframework.boot:spring-boot-starter-validation")
|
||||
implementation("org.springframework.boot:spring-boot-starter-web")
|
||||
implementation("jakarta.validation:jakarta.validation-api")
|
||||
|
||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
|
||||
implementation("org.springframework.boot:spring-boot-starter-webflux")
|
||||
// implementation("org.springdoc:springdoc-openapi-starter-webflux-ui:${property("versions.open-api")}")
|
||||
|
||||
implementation("org.springframework.kafka:spring-kafka")
|
||||
implementation("org.springframework.cloud:spring-cloud-starter-config")
|
||||
|
||||
implementation("org.springframework.boot:spring-boot-starter-flyway")
|
||||
implementation("org.flywaydb:flyway-database-postgresql")
|
||||
runtimeOnly("org.postgresql:postgresql")
|
||||
implementation("jakarta.validation:jakarta.validation-api")
|
||||
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
|
||||
|
||||
testImplementation("junit:junit")
|
||||
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
|
||||
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
||||
testImplementation("org.springframework.security:spring-security-test")
|
||||
testImplementation("org.testcontainers:junit-jupiter")
|
||||
|
||||
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||
}
|
||||
|
||||
dependencyManagement {
|
||||
imports {
|
||||
mavenBom("org.springframework.cloud:spring-cloud-dependencies:${property("versions.spring.cloud")}")
|
||||
mavenBom("org.testcontainers:testcontainers-bom:${property("versions.testcontainers")}")
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType<Jar> {
|
||||
manifest {
|
||||
attributes["Built-By"] = "nstart"
|
||||
attributes["Implementation-Version"] = archiveVersion
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType<Test> {
|
||||
enabled = true
|
||||
useJUnitPlatform()
|
||||
finalizedBy(tasks.jacocoTestReport)
|
||||
systemProperty("spring.profiles.active", "test")
|
||||
}
|
||||
|
||||
tasks.check {
|
||||
dependsOn(tasks.jacocoTestCoverageVerification)
|
||||
}
|
||||
|
||||
tasks.jacocoTestReport {
|
||||
dependsOn(tasks.test)
|
||||
reports {
|
||||
xml.required = true
|
||||
html.required = true
|
||||
csv.required = false
|
||||
}
|
||||
}
|
||||
|
||||
sonar {
|
||||
properties {
|
||||
property("sonar.projectKey", "pcp")
|
||||
property("sonar.login", "sqp_tokenExample")
|
||||
property("sonar.qualitygate.wait", "${property("sonar.qualitygate.wait")}")
|
||||
property("sonar.host.url", "${property("sonar.host.url")}")
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package space.nstart.pcp.tle_monitoring_service
|
||||
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.context.properties.ConfigurationPropertiesScan
|
||||
import org.springframework.boot.runApplication
|
||||
import org.springframework.scheduling.annotation.EnableScheduling
|
||||
|
||||
@SpringBootApplication
|
||||
@ConfigurationPropertiesScan
|
||||
@EnableScheduling
|
||||
class TleMonitoringServiceApplication
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
runApplication<TleMonitoringServiceApplication>(*args)
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package space.nstart.pcp.tle_monitoring_service.configuration
|
||||
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.validation.FieldError
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler
|
||||
import org.springframework.web.bind.support.WebExchangeBindException
|
||||
import reactor.core.publisher.Mono
|
||||
import kotlin.collections.forEach
|
||||
import kotlin.collections.set
|
||||
import kotlin.to
|
||||
|
||||
class CustomValidationException(message: String) : RuntimeException(message)
|
||||
class CustomErrorException(message: String) : RuntimeException(message)
|
||||
|
||||
|
||||
@ControllerAdvice
|
||||
class GlobalExceptionHandler {
|
||||
|
||||
@ExceptionHandler(CustomValidationException::class)
|
||||
fun handleValidation(ex: CustomValidationException): ResponseEntity<Map<String, String>> {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(mapOf("error" to ex.message!!))
|
||||
}
|
||||
@ExceptionHandler(CustomErrorException::class)
|
||||
fun handleError(ex: CustomErrorException): ResponseEntity<Map<String, String>> {
|
||||
return ResponseEntity.internalServerError()
|
||||
.body(mapOf("error" to ex.message!!))
|
||||
}
|
||||
}
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
package space.nstart.pcp.tle_monitoring_service.configuration
|
||||
|
||||
import org.apache.kafka.clients.admin.AdminClientConfig
|
||||
import org.apache.kafka.clients.admin.NewTopic
|
||||
import org.apache.kafka.clients.consumer.ConsumerConfig
|
||||
import org.apache.kafka.clients.consumer.ConsumerRecord
|
||||
import org.apache.kafka.clients.producer.ProducerConfig
|
||||
import org.apache.kafka.common.header.Header
|
||||
import org.apache.kafka.common.serialization.StringDeserializer
|
||||
import org.apache.kafka.common.serialization.StringSerializer
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory
|
||||
import org.springframework.kafka.core.ConsumerFactory
|
||||
import org.springframework.kafka.core.DefaultKafkaConsumerFactory
|
||||
import org.springframework.kafka.core.DefaultKafkaProducerFactory
|
||||
import org.springframework.kafka.core.KafkaAdmin
|
||||
import org.springframework.kafka.core.KafkaTemplate
|
||||
import org.springframework.kafka.core.ProducerFactory
|
||||
import org.springframework.kafka.listener.adapter.RecordFilterStrategy
|
||||
import space.nstart.pcp.pcp_types_lib.message.PcpKafkaEvent
|
||||
import java.nio.charset.StandardCharsets
|
||||
import kotlin.String
|
||||
|
||||
|
||||
@ConditionalOnProperty(name=["spring.kafka.bootstrap-servers"], matchIfMissing = false)
|
||||
@Configuration
|
||||
class KafkaConfig {
|
||||
companion object {
|
||||
const val HEADER_NAME: String = "type"
|
||||
const val TARGET_CONSUMER_HEADER_NAME: String = "target_consumer"
|
||||
}
|
||||
|
||||
@Value("\${spring.kafka.template.default-topic}")
|
||||
private lateinit var defaultTopic: String
|
||||
|
||||
@Bean
|
||||
fun tleTopic(): NewTopic =
|
||||
NewTopic(defaultTopic, 1, 1)
|
||||
|
||||
|
||||
@Value("\${spring.kafka.bootstrap-servers}")
|
||||
private lateinit var addr: String
|
||||
|
||||
@Value("\${spring.kafka.consumer.group-id}")
|
||||
private lateinit var groupId: String
|
||||
|
||||
@Bean
|
||||
fun template(): KafkaTemplate<String, Any> {
|
||||
return KafkaTemplate(producerFactory())
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun kafkaAdmin(): KafkaAdmin {
|
||||
|
||||
val configs = hashMapOf<String, Any>()
|
||||
configs[AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG] = addr
|
||||
|
||||
return KafkaAdmin(configs)
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun consumerFactory(): ConsumerFactory<String, Any> {
|
||||
|
||||
val props = hashMapOf<String, Any>()
|
||||
props[ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG] = addr
|
||||
props[ConsumerConfig.GROUP_ID_CONFIG] = groupId
|
||||
props[ConsumerConfig.AUTO_OFFSET_RESET_CONFIG] = "latest"
|
||||
props[ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG] = false
|
||||
props[ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG] = StringDeserializer::class.java
|
||||
props[ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG] = StringDeserializer::class.java
|
||||
|
||||
return DefaultKafkaConsumerFactory(props)
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun kafkaListenerContainerFactory(): ConcurrentKafkaListenerContainerFactory<String, Any> =
|
||||
ConcurrentKafkaListenerContainerFactory<String, Any>()
|
||||
.apply { setConsumerFactory(consumerFactory()) }
|
||||
|
||||
@Bean
|
||||
fun producerFactory(): ProducerFactory<String, Any> {
|
||||
|
||||
val configProps = hashMapOf<String, Any>()
|
||||
configProps[ProducerConfig.BOOTSTRAP_SERVERS_CONFIG] = addr
|
||||
configProps[ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG] = StringSerializer::class.java
|
||||
configProps[ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG] = StringSerializer::class.java
|
||||
|
||||
return DefaultKafkaProducerFactory(configProps)
|
||||
}
|
||||
|
||||
private fun getRecordFilterStrategy(event: PcpKafkaEvent): RecordFilterStrategy<String, String> {
|
||||
return RecordFilterStrategy { consumerRecord: ConsumerRecord<String, String> ->
|
||||
val eventTypeHeader: Header? = consumerRecord.headers().lastHeader(HEADER_NAME)
|
||||
if (eventTypeHeader != null) {
|
||||
val headerValue = String(eventTypeHeader.value(), StandardCharsets.UTF_8)
|
||||
return@RecordFilterStrategy event.name != headerValue
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun getRecordFilterStrategyByConsumer(): RecordFilterStrategy<String, String> {
|
||||
return RecordFilterStrategy { consumerRecord: ConsumerRecord<String, String> ->
|
||||
val consumerGroupHeader: Header? = consumerRecord.headers().lastHeader(TARGET_CONSUMER_HEADER_NAME)
|
||||
if (consumerGroupHeader != null) {
|
||||
val headerValue = String(consumerGroupHeader.value(), StandardCharsets.UTF_8)
|
||||
return@RecordFilterStrategy "respondedListener" != headerValue
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
@Bean("placedIcFilter")
|
||||
fun placedFilter(): RecordFilterStrategy<String, String> {
|
||||
return getRecordFilterStrategy(PcpKafkaEvent.ICPlacedEvent)
|
||||
}
|
||||
|
||||
@Bean("updatedIcFilter")
|
||||
fun updatedIcFilter(): RecordFilterStrategy<String, String> {
|
||||
return getRecordFilterStrategy(PcpKafkaEvent.ICUpdatedEvent)
|
||||
}
|
||||
|
||||
@Bean("satelliteDeletedFilter")
|
||||
fun satelliteDeletedFilter(): RecordFilterStrategy<String, String> {
|
||||
return getRecordFilterStrategy(PcpKafkaEvent.SatelliteDeletedEvent)
|
||||
}
|
||||
|
||||
@Bean("respondedFilter")
|
||||
fun respondedFilter(): RecordFilterStrategy<String, String> {
|
||||
return getRecordFilterStrategyByConsumer()
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package space.nstart.pcp.tle_monitoring_service.configuration
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||
|
||||
@ConfigurationProperties(prefix = "tle.polling")
|
||||
data class TlePollingProperties(
|
||||
var enabled: Boolean = false,
|
||||
var timeoutSeconds: Long = 10_800,
|
||||
)
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package space.nstart.pcp.tle_monitoring_service.configuration
|
||||
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.http.client.reactive.ReactorClientHttpConnector
|
||||
import org.springframework.web.reactive.function.client.WebClient
|
||||
import reactor.netty.http.client.HttpClient
|
||||
import java.time.Duration
|
||||
|
||||
@Configuration
|
||||
class WebClientConfig {
|
||||
|
||||
@Bean
|
||||
fun webClientBuilder(): WebClient.Builder {
|
||||
val httpClient = HttpClient.create()
|
||||
.responseTimeout(Duration.ofSeconds(10))
|
||||
|
||||
return WebClient.builder()
|
||||
.clientConnector(ReactorClientHttpConnector(httpClient))
|
||||
.codecs { configurer ->
|
||||
configurer.defaultCodecs().maxInMemorySize(16 * 1024 * 1024)
|
||||
}
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun webClient(webClientBuilder: WebClient.Builder): WebClient = webClientBuilder.build()
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
package space.nstart.pcp.tle_monitoring_service.controller
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
import org.springframework.web.bind.annotation.PostMapping
|
||||
import org.springframework.web.bind.annotation.RequestBody
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RequestParam
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TLEDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TLEExtensionDTO
|
||||
import space.nstart.pcp.tle_monitoring_service.repository.TLERepository
|
||||
import space.nstart.pcp.tle_monitoring_service.service.CelesTrakService
|
||||
|
||||
|
||||
@RestController
|
||||
@RequestMapping("v1/api/tle")
|
||||
class TLEController {
|
||||
|
||||
|
||||
@Autowired
|
||||
private lateinit var tleRepository: TLERepository
|
||||
@Autowired
|
||||
private lateinit var celestrakService: CelesTrakService
|
||||
|
||||
@GetMapping()
|
||||
fun all() = tleRepository.findAll().map { tle ->
|
||||
val t = tle.tle.split("\r\n")
|
||||
TLEExtensionDTO(
|
||||
tle.satelliteId,
|
||||
tle.revolution,
|
||||
TLEDTO(t[0], t[1], t[2])
|
||||
)
|
||||
}
|
||||
|
||||
// @GetMapping("/test")
|
||||
// fun update() = celestrakService.updateDB()
|
||||
|
||||
|
||||
@PostMapping("load-array")
|
||||
fun arra(@RequestBody data : String, @RequestParam name : String) =
|
||||
celestrakService.loadArray(data, name)
|
||||
|
||||
|
||||
@PostMapping ("check-cycle")
|
||||
fun checkCycle(@RequestParam id : Long) =
|
||||
celestrakService.checkCycle(id)
|
||||
|
||||
@PostMapping("from-celestrack")
|
||||
fun tleFromSource(@RequestParam noradIn : Long, @RequestParam("message") sendMessage : Boolean) =
|
||||
celestrakService.getTleByNoradIdRaw(noradIn, false, sendMessage)
|
||||
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package space.nstart.pcp.tle_monitoring_service.controller
|
||||
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.PutMapping
|
||||
import org.springframework.web.bind.annotation.RequestBody
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import space.nstart.pcp.tle_monitoring_service.service.TlePollingSettingsService
|
||||
import space.nstart.pcp.tle_monitoring_service.service.UpdateTlePollingSettingsRequest
|
||||
|
||||
@RestController
|
||||
@RequestMapping("v1/api/tle/polling-settings")
|
||||
class TlePollingSettingsController(
|
||||
private val pollingSettingsService: TlePollingSettingsService,
|
||||
) {
|
||||
|
||||
@GetMapping
|
||||
fun current() = pollingSettingsService.current()
|
||||
|
||||
@PutMapping
|
||||
fun update(@RequestBody request: UpdateTlePollingSettingsRequest) =
|
||||
pollingSettingsService.update(request)
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package space.nstart.pcp.tle_monitoring_service.entity
|
||||
|
||||
import jakarta.persistence.*
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneOffset
|
||||
|
||||
|
||||
@Entity
|
||||
@Table(name = "tle")
|
||||
class TLEEntity (
|
||||
@Id
|
||||
@Column(name = "tle_id")
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
val id : Long? = null,
|
||||
|
||||
@Column(nullable = false)
|
||||
val satelliteId : Long = 0,
|
||||
|
||||
@Column(nullable = false)
|
||||
val revolution : Long = 0,
|
||||
|
||||
@Column(nullable = false)
|
||||
val tle : String = "",
|
||||
var timeTle : LocalDateTime? = null,
|
||||
@Column(name = "time_rec", nullable = false)
|
||||
val timeRec : LocalDateTime = LocalDateTime.now(ZoneOffset.UTC)
|
||||
)
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
package space.nstart.pcp.tle_monitoring_service.message
|
||||
|
||||
import org.apache.kafka.clients.producer.ProducerRecord
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
|
||||
import org.springframework.kafka.core.KafkaTemplate
|
||||
import org.springframework.kafka.support.SendResult
|
||||
import org.springframework.stereotype.Component
|
||||
import space.nstart.pcp.pcp_types_lib.message.KafkaMessage
|
||||
import space.nstart.pcp.tle_monitoring_service.configuration.KafkaConfig
|
||||
import tools.jackson.core.JacksonException
|
||||
import tools.jackson.databind.ObjectMapper
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.function.BiConsumer
|
||||
import kotlin.Any
|
||||
import kotlin.String
|
||||
|
||||
|
||||
@ConditionalOnProperty(name=["spring.kafka.bootstrap-servers"], matchIfMissing = false)
|
||||
@Component
|
||||
class KafkaProducer {
|
||||
|
||||
@Value("\${spring.kafka.template.default-topic}")
|
||||
private val defaultTopic: String? = null
|
||||
|
||||
@Value("\${spring.application.name:opkat-ks-request-service}")
|
||||
private val applicationName: String = ""
|
||||
|
||||
@Autowired
|
||||
private lateinit var kafkaTemplate: KafkaTemplate<String, Any>
|
||||
|
||||
@Autowired
|
||||
private lateinit var objectMapper: ObjectMapper
|
||||
|
||||
private val log: Logger = LoggerFactory.getLogger(this::class.java)
|
||||
|
||||
fun send(message: KafkaMessage<Any>, topic: String? = null, extraHeaders: Map<String, Any>? = null) {
|
||||
try {
|
||||
message.source = applicationName
|
||||
|
||||
val jsonMessage = objectMapper.writeValueAsString(message)
|
||||
val selectedTopic = topic ?: defaultTopic
|
||||
val record: ProducerRecord<String, Any> = ProducerRecord(
|
||||
selectedTopic,
|
||||
message.traceId,
|
||||
jsonMessage
|
||||
)
|
||||
record.headers().add(KafkaConfig.HEADER_NAME, message.type.enumCast()?.name?.toByteArray(StandardCharsets.UTF_8))
|
||||
extraHeaders?.forEach { record.headers().add(it.key, it.value.toString().toByteArray(StandardCharsets.UTF_8)) }
|
||||
val future: CompletableFuture<SendResult<String, Any>> = kafkaTemplate.send(record)
|
||||
future.whenCompleteAsync { result: SendResult<String, Any>, ex: Throwable? ->
|
||||
if (ex == null) log.debug(
|
||||
"В Kafka отправлено сообщение. Topic: {}. Header: {}. Key: {}. Value: {}.",
|
||||
result.producerRecord.topic(),
|
||||
result.producerRecord.headers().lastHeader(KafkaConfig.HEADER_NAME),
|
||||
result.producerRecord.key(),
|
||||
result.producerRecord.value()
|
||||
)
|
||||
else log.error(
|
||||
"Ошибка отправки сообщения в Kafka. Topic: {}. Header: {}. Key: {}. Value: {}.",
|
||||
selectedTopic,
|
||||
message.type.enumCast()?.name,
|
||||
message.traceId,
|
||||
jsonMessage
|
||||
)
|
||||
}
|
||||
} catch (e: JacksonException) {
|
||||
throw RuntimeException("Ошибка конвертации сообщения в json: " + e.message, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
package space.nstart.pcp.tle_monitoring_service.message
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.beans.factory.ObjectProvider
|
||||
import org.springframework.kafka.annotation.KafkaListener
|
||||
import org.springframework.stereotype.Component
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteDeletedEventDTO
|
||||
import space.nstart.pcp.pcp_types_lib.message.KafkaMessage
|
||||
import space.nstart.pcp.tle_monitoring_service.service.SatelliteDeletedService
|
||||
|
||||
private const val SATELLITE_DELETED_CONSUMER_GROUP = "pcp-tle-monitoring-service-satellite-deleted"
|
||||
|
||||
@Component
|
||||
class SatelliteDeletedKafkaListener(
|
||||
objectMapperProvider: ObjectProvider<ObjectMapper>,
|
||||
private val satelliteDeletedService: SatelliteDeletedService
|
||||
) {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(this::class.java)
|
||||
private val objectMapper = objectMapperProvider.ifAvailable ?: jacksonObjectMapper().findAndRegisterModules()
|
||||
|
||||
@KafkaListener(
|
||||
id = "satelliteDeletedFilter",
|
||||
topics = ["\${app.kafka.topics.satellites:pcp.satellites}"],
|
||||
groupId = SATELLITE_DELETED_CONSUMER_GROUP,
|
||||
filter = "satelliteDeletedFilter"
|
||||
)
|
||||
fun consume(message: String?) {
|
||||
if (message.isNullOrBlank()) {
|
||||
logger.warn("Received empty SatelliteDeletedEvent Kafka message")
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
val kafkaMessage = objectMapper.readValue(
|
||||
message,
|
||||
object : TypeReference<KafkaMessage<SatelliteDeletedEventDTO>>() {}
|
||||
)
|
||||
val event = kafkaMessage.data
|
||||
logger.info(
|
||||
"Received SatelliteDeletedEvent in tle-monitoring-service: satelliteId={}, noradId={}, eventId={}, traceId={}, source={}, consumerGroup={}",
|
||||
event.satelliteId,
|
||||
event.noradId,
|
||||
kafkaMessage.id,
|
||||
kafkaMessage.traceId,
|
||||
kafkaMessage.source,
|
||||
SATELLITE_DELETED_CONSUMER_GROUP
|
||||
)
|
||||
event.deleteIdentifiers().forEach { satelliteId ->
|
||||
satelliteDeletedService.deleteSatelliteData(satelliteId)
|
||||
}
|
||||
logger.info(
|
||||
"SatelliteDeletedEvent processed in tle-monitoring-service: satelliteId={}, noradId={}, eventId={}, traceId={}, source={}, consumerGroup={}",
|
||||
event.satelliteId,
|
||||
event.noradId,
|
||||
kafkaMessage.id,
|
||||
kafkaMessage.traceId,
|
||||
kafkaMessage.source,
|
||||
SATELLITE_DELETED_CONSUMER_GROUP
|
||||
)
|
||||
} catch (exception: Exception) {
|
||||
logger.error("Failed to process SatelliteDeletedEvent in tle-monitoring-service", exception)
|
||||
throw exception
|
||||
}
|
||||
}
|
||||
|
||||
private fun SatelliteDeletedEventDTO.deleteIdentifiers(): List<Long> =
|
||||
listOfNotNull(satelliteId, noradId).distinct()
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package space.nstart.pcp.tle_monitoring_service.repository
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import space.nstart.pcp.tle_monitoring_service.entity.TLEEntity
|
||||
|
||||
interface TLERepository : JpaRepository<TLEEntity, Long> {
|
||||
|
||||
fun findBySatelliteIdAndRevolution(id : Long, rev : Long) : List<TLEEntity>
|
||||
|
||||
fun findAllBySatelliteId(id : Long) : List<TLEEntity>
|
||||
|
||||
fun countBySatelliteId(id: Long): Long
|
||||
|
||||
fun deleteAllBySatelliteId(id: Long): Long
|
||||
}
|
||||
+330
@@ -0,0 +1,330 @@
|
||||
package space.nstart.pcp.tle_monitoring_service.service
|
||||
|
||||
import ballistics.Ballistics
|
||||
import ballistics.types.BallisticsError
|
||||
import ballistics.types.TLE
|
||||
import ballistics.utils.toDateTime
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.scheduling.annotation.Scheduled
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.web.reactive.function.client.WebClient
|
||||
import org.springframework.web.reactive.function.client.WebClientResponseException
|
||||
import org.springframework.web.reactive.function.client.bodyToMono
|
||||
import org.springframework.web.util.UriComponentsBuilder
|
||||
import reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.Mono
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.TLEDTO
|
||||
import space.nstart.pcp.pcp_types_lib.message.KafkaMessage
|
||||
import space.nstart.pcp.pcp_types_lib.message.PcpKafkaEvent
|
||||
import space.nstart.pcp.tle_monitoring_service.configuration.CustomErrorException
|
||||
import space.nstart.pcp.tle_monitoring_service.entity.TLEEntity
|
||||
import space.nstart.pcp.tle_monitoring_service.message.KafkaProducer
|
||||
import space.nstart.pcp.tle_monitoring_service.repository.TLERepository
|
||||
import java.time.Duration
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.abs
|
||||
import kotlin.text.split
|
||||
|
||||
|
||||
@Service
|
||||
class CelesTrakService(
|
||||
private val webClient: WebClient,
|
||||
private val pollingSettingsService: TlePollingSettingsService,
|
||||
) {
|
||||
private val logger = LoggerFactory.getLogger(this::class.java)
|
||||
private val objectMapper = jacksonObjectMapper()
|
||||
|
||||
@Autowired
|
||||
private lateinit var sender: KafkaProducer
|
||||
|
||||
@Autowired
|
||||
private lateinit var tleRepository: TLERepository
|
||||
|
||||
@Autowired
|
||||
private lateinit var complexMissionService: ComplexMissionService
|
||||
|
||||
companion object {
|
||||
private const val BASE_URL = "https://celestrak.org"
|
||||
private const val USER_AGENT = "SatelliteTracker/1.0 (Spring Boot)"
|
||||
}
|
||||
|
||||
|
||||
@Scheduled(fixedDelay = 1000)
|
||||
fun findExpired(): Flux<TLEDTO> {
|
||||
if (!pollingSettingsService.shouldStartPolling()) {
|
||||
return Flux.empty()
|
||||
}
|
||||
|
||||
val settings = pollingSettingsService.current()
|
||||
pollingSettingsService.markPollingStarted()
|
||||
logger.info("Запуск цикла опроса TLE. timeoutSeconds=${settings.timeoutSeconds}")
|
||||
|
||||
return complexMissionService.getSatellites().filter { s -> s.scanTLE }.flatMap {
|
||||
sat ->
|
||||
getTleByNoradIdRaw(sat.noradId, send = true, sendForce = false)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
fun updateDB(){
|
||||
val bal = Ballistics()
|
||||
tleRepository.findAll().forEach { tle ->
|
||||
try{
|
||||
val dat = tle.tle.split("\r\n")
|
||||
val params = bal.getTLEParams(TLE(dat[1], dat[2]))
|
||||
tleRepository.save(tle.apply {
|
||||
timeTle = params.time
|
||||
})
|
||||
|
||||
} catch (ex : Exception){
|
||||
logger.warn("Ошибка разбора TLE ${ex.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
fun loadArray(data : String, name : String){
|
||||
|
||||
val tles = data.split("\n")
|
||||
var first = true
|
||||
var s1 : String = ""
|
||||
var s2 : String
|
||||
for (s in tles){
|
||||
if (first){
|
||||
s1 = s
|
||||
} else {
|
||||
s2 = s
|
||||
val id = getNoradIdFromTLE(s1)
|
||||
mkTle(TLEDTO(name,s1, s2), id, false, false)
|
||||
}
|
||||
first = ! first
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun getTleArrayByNoradIdRaw(noradId: Long, name: String): Mono<Void> {
|
||||
// Используем UriComponentsBuilder для корректного построения URL
|
||||
val uri = UriComponentsBuilder.fromUriString("https://www.space-track.org/basicspacedata/query/class/gp_history/norad_cat_id/$noradId/CREATION_DATE/%3Enow-60/format/tle")
|
||||
.build()
|
||||
.toUri()
|
||||
logger.info("Запрос архива TLE для NORAD ID: $noradId, URI: $uri")
|
||||
return webClient.get()
|
||||
.uri(uri)
|
||||
.retrieve()
|
||||
.bodyToMono<String>()
|
||||
.timeout(Duration.ofSeconds(15))
|
||||
.doOnNext { rawText ->
|
||||
loadArray(rawText, name)
|
||||
}
|
||||
.doOnError { error ->
|
||||
handleError(error, noradId, "raw-array")
|
||||
}
|
||||
.then()
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Получение TLE в сыром текстовом формате
|
||||
*/
|
||||
fun getTleByNoradIdRaw(noradId: Long, send : Boolean, sendForce : Boolean): Mono<TLEDTO> {
|
||||
// Используем UriComponentsBuilder для корректного построения URL
|
||||
val uri = UriComponentsBuilder.fromUriString("$BASE_URL/NORAD/elements/gp.php")
|
||||
.queryParam("CATNR", noradId)
|
||||
.queryParam("FORMAT", "tle")
|
||||
.build()
|
||||
.toUri()
|
||||
logger.info("Запрос raw TLE для NORAD ID: $noradId, URI: $uri")
|
||||
return webClient.get()
|
||||
.uri(uri)
|
||||
.header("User-Agent", USER_AGENT)
|
||||
.retrieve()
|
||||
.bodyToMono<String>()
|
||||
.timeout(Duration.ofSeconds(15))
|
||||
.map { rawText ->
|
||||
parseRawTle(rawText, noradId)
|
||||
}
|
||||
.onErrorResume { error ->
|
||||
handleError(error, noradId, "raw")
|
||||
Mono.just(TLEDTO())
|
||||
}
|
||||
.doOnSuccess { tle ->
|
||||
if (tle != null) {
|
||||
mkTle(tle, noradId, send, sendForce)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun mkTle(tle : TLEDTO, noradId : Long, send : Boolean, sendForce : Boolean){
|
||||
val rev = getRevolutionNumberFromTLE(tle.second)
|
||||
if (rev <= 0)
|
||||
logger.error("Не удалось прочитать номер витка из строки ${tle.second} для id $noradId")
|
||||
else {
|
||||
|
||||
|
||||
if (tleRepository.findBySatelliteIdAndRevolution(noradId, rev).isEmpty()) {
|
||||
|
||||
val bal = Ballistics()
|
||||
|
||||
try {
|
||||
|
||||
val params = bal.getTLEParams(TLE(tle.first, tle.second))
|
||||
tleRepository.save(
|
||||
TLEEntity(
|
||||
null,
|
||||
noradId,
|
||||
rev,
|
||||
"${tle.header ?: "---"}\r\n${tle.first}\r\n${tle.second}",
|
||||
timeTle = params.time
|
||||
|
||||
)
|
||||
)
|
||||
logger.info("Данные TLE для noradId $noradId сохранены в БД")
|
||||
} catch (ex: Exception) {
|
||||
logger.warn("Ошибка разбора TLE ${ex.message}")
|
||||
throw CustomErrorException("Ошибка разбора TLE ${ex.message}")
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (send && !sendForce) {
|
||||
sender.send(
|
||||
KafkaMessage(
|
||||
type = PcpKafkaEvent.ICUpdatedEvent,
|
||||
data = tle,
|
||||
traceId = UUID.randomUUID().toString()
|
||||
)
|
||||
)
|
||||
logger.info("Данные TLE для noradId $noradId отправлены в очередь сообщений")
|
||||
}
|
||||
} else
|
||||
logger.info("Данные TLE для noradId $noradId не изменились")
|
||||
|
||||
|
||||
if (sendForce) {
|
||||
logger.info("Данные TLE для noradId $noradId принудительно отправлены в очередь сообщений")
|
||||
sender.send(
|
||||
KafkaMessage(
|
||||
type = PcpKafkaEvent.ICUpdatedEvent,
|
||||
data = tle,
|
||||
traceId = UUID.randomUUID().toString()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getRevolutionNumberFromTLE(line2: String): Long {
|
||||
// Номер витка находится в позициях 64-68 (индексы 63-67)
|
||||
if (line2.length >= 68) {
|
||||
val revNumberStr = line2.substring(63, 68).trim()
|
||||
return revNumberStr.toLongOrNull() ?: 0
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
fun getNoradIdFromTLE(line1: String): Long {
|
||||
// Номер витка находится в позициях 64-68 (индексы 63-67)
|
||||
if (line1.length >= 15) {
|
||||
val noradId = line1.substring(2, 7).trim()
|
||||
return noradId.toLongOrNull() ?: 0
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Парсинг сырого TLE текста
|
||||
*/
|
||||
private fun parseRawTle(rawText: String, noradId: Long): TLEDTO {
|
||||
val lines = rawText.lines()
|
||||
.map { it.trim() }
|
||||
.filter { it.isNotBlank() }
|
||||
return when {
|
||||
lines.size >= 3 -> {
|
||||
TLEDTO(
|
||||
lines[0],
|
||||
lines[1],
|
||||
lines[2]
|
||||
)
|
||||
}
|
||||
lines.size == 2 -> {
|
||||
// Если нет имени объекта
|
||||
TLEDTO(
|
||||
"NORAD-$noradId",
|
||||
lines[0],
|
||||
lines[1]
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
logger.warn("Некорректный TLE формат для NORAD ID: $noradId")
|
||||
throw CustomErrorException("Не удалось получить TLE: $lines")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleError(error: Throwable, noradId: Long, format: String) {
|
||||
when (error) {
|
||||
is WebClientResponseException -> {
|
||||
logger.error("HTTP ошибка при запросе $format TLE для NORAD ID: $noradId. " +
|
||||
"Статус: ${error.statusCode}, Тело: ${error.responseBodyAsString}", error)
|
||||
throw CustomErrorException("HTTP ошибка при запросе $format TLE для NORAD ID: $noradId. " +
|
||||
"Статус: ${error.statusCode}, Тело: ${error.responseBodyAsString}")
|
||||
}
|
||||
is java.net.UnknownHostException -> {
|
||||
logger.error("Не удалось разрешить хост для запроса TLE (NORAD ID: $noradId). " +
|
||||
"Проверьте подключение к интернету.", error)
|
||||
throw CustomErrorException("Не удалось разрешить хост для запроса TLE (NORAD ID: $noradId). " +
|
||||
"Проверьте подключение к интернету.")
|
||||
}
|
||||
is java.net.SocketTimeoutException -> {
|
||||
logger.error("Таймаут при запросе TLE для NORAD ID: $noradId", error)
|
||||
throw CustomErrorException("Таймаут при запросе TLE для NORAD ID: $noradId")
|
||||
}
|
||||
else -> {
|
||||
logger.error("Ошибка при запросе $format TLE для NORAD ID: $noradId", error)
|
||||
throw CustomErrorException("Ошибка при запросе $format TLE для NORAD ID: $noradId")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// https://www.space-track.org/basicspacedata/query/class/gp_history/norad_cat_id/39634/CREATION_DATE/%3Enow-60/format/tle
|
||||
fun checkCycle(id : Long){
|
||||
val bal = Ballistics()
|
||||
val tles = tleRepository.findAllBySatelliteId(id)
|
||||
.sortedBy { tle -> tle.timeTle }
|
||||
var first = true
|
||||
var vit = 0
|
||||
var tVuz = LocalDateTime.now()
|
||||
var lVuz = 0.0
|
||||
for(t in tles){
|
||||
var s = t.tle.split("\r\n")
|
||||
val r = bal.calculateOrbPoints(TLE(s[1],s[2]), 86400.0 * 10)
|
||||
if (r != BallisticsError.OK)
|
||||
throw CustomErrorException("Ошибка расчета прогноза")
|
||||
if (first){
|
||||
first = false
|
||||
vit = bal.revolutions.first().vuz.vit
|
||||
lVuz = bal.revolutions.first().lVuz * 180.0 / PI
|
||||
tVuz = toDateTime(bal.revolutions.first().vuz.t)
|
||||
} else {
|
||||
for (v in bal.revolutions){
|
||||
val dL = abs(v.lVuz* 180.0 / PI - lVuz)
|
||||
val dvit = v.vuz.vit-vit
|
||||
if (dL < 0.005){
|
||||
val dt : Long = Duration.between(tVuz, toDateTime(v.vuz.t)).seconds
|
||||
logger.info("${tVuz} - ${toDateTime(v.vuz.t)} сдвиг витков ${abs(vit - v.vuz.vit)} : ${dL * PI / 180.0} : ${v.vuz.r.z} : $dt : ${dt/86400}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package space.nstart.pcp.tle_monitoring_service.service
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.web.reactive.function.client.WebClient
|
||||
import reactor.core.publisher.Flux
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteSummaryDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteInfoDTO
|
||||
|
||||
|
||||
@Service
|
||||
class ComplexMissionService
|
||||
(private val webClientBuilder: WebClient.Builder) {
|
||||
|
||||
@Value("\${settings.satellite-catalog-service:\${settings.complex-mission-service:pcp-satellite-catalog-service}}")
|
||||
val satelliteCatalogServiceURI = "pcp-satellite-catalog-service"
|
||||
|
||||
|
||||
fun getSatellites(): Flux<SatelliteInfoDTO> =
|
||||
webClientBuilder.build()
|
||||
.get()
|
||||
.uri("$satelliteCatalogServiceURI/api/satellites")
|
||||
.retrieve()
|
||||
.bodyToFlux(SatelliteSummaryDTO::class.java)
|
||||
.map { satellite ->
|
||||
SatelliteInfoDTO(
|
||||
noradId = satellite.id,
|
||||
name = satellite.name,
|
||||
red = satellite.visualization.red,
|
||||
green = satellite.visualization.green,
|
||||
blue = satellite.visualization.blue,
|
||||
scanTLE = satellite.scanTle,
|
||||
code = satellite.code,
|
||||
typeCode = satellite.typeCode,
|
||||
active = satellite.active
|
||||
)
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package space.nstart.pcp.tle_monitoring_service.service
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import space.nstart.pcp.tle_monitoring_service.repository.TLERepository
|
||||
|
||||
@Service
|
||||
class SatelliteDeletedService(
|
||||
private val tleRepository: TLERepository
|
||||
) {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(this::class.java)
|
||||
|
||||
@Transactional
|
||||
fun deleteSatelliteData(satelliteId: Long): Long {
|
||||
val beforeDelete = tleRepository.countBySatelliteId(satelliteId)
|
||||
val deleted = tleRepository.deleteAllBySatelliteId(satelliteId)
|
||||
logger.info(
|
||||
"Deleted TLE data for satellite: satelliteId={}, tleBeforeDelete={}, tleDeleted={}",
|
||||
satelliteId,
|
||||
beforeDelete,
|
||||
deleted
|
||||
)
|
||||
return deleted
|
||||
}
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
package space.nstart.pcp.tle_monitoring_service.service
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.stereotype.Service
|
||||
import space.nstart.pcp.tle_monitoring_service.configuration.CustomValidationException
|
||||
import space.nstart.pcp.tle_monitoring_service.configuration.TlePollingProperties
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
data class TlePollingSettings(
|
||||
val enabled: Boolean,
|
||||
val timeoutSeconds: Long,
|
||||
)
|
||||
|
||||
data class UpdateTlePollingSettingsRequest(
|
||||
val enabled: Boolean?,
|
||||
val timeoutSeconds: Long?,
|
||||
)
|
||||
|
||||
@Service
|
||||
class TlePollingSettingsService(properties: TlePollingProperties) {
|
||||
private val logger = LoggerFactory.getLogger(this::class.java)
|
||||
private val enabled = AtomicBoolean(properties.enabled)
|
||||
private val timeoutSeconds = AtomicLong(validateTimeout(properties.timeoutSeconds))
|
||||
private val lastPollStartedAt = AtomicReference<Instant?>()
|
||||
|
||||
fun current(): TlePollingSettings =
|
||||
TlePollingSettings(
|
||||
enabled = enabled.get(),
|
||||
timeoutSeconds = timeoutSeconds.get(),
|
||||
)
|
||||
|
||||
fun update(request: UpdateTlePollingSettingsRequest): TlePollingSettings {
|
||||
val updatedEnabled = request.enabled
|
||||
?: throw CustomValidationException("enabled must be set")
|
||||
val updatedTimeoutSeconds = validateTimeout(
|
||||
request.timeoutSeconds ?: throw CustomValidationException("timeoutSeconds must be set")
|
||||
)
|
||||
|
||||
val wasEnabled = enabled.getAndSet(updatedEnabled)
|
||||
timeoutSeconds.set(updatedTimeoutSeconds)
|
||||
if (!wasEnabled && updatedEnabled) {
|
||||
lastPollStartedAt.set(null)
|
||||
}
|
||||
|
||||
val settings = current()
|
||||
logger.info(
|
||||
"TLE polling settings updated: enabled={}, timeoutSeconds={}",
|
||||
settings.enabled,
|
||||
settings.timeoutSeconds,
|
||||
)
|
||||
return settings
|
||||
}
|
||||
|
||||
fun shouldStartPolling(now: Instant = Instant.now()): Boolean {
|
||||
if (!enabled.get()) {
|
||||
return false
|
||||
}
|
||||
|
||||
val lastStartedAt = lastPollStartedAt.get() ?: return true
|
||||
return Duration.between(lastStartedAt, now).seconds >= timeoutSeconds.get()
|
||||
}
|
||||
|
||||
fun markPollingStarted(now: Instant = Instant.now()) {
|
||||
lastPollStartedAt.set(now)
|
||||
}
|
||||
|
||||
private fun validateTimeout(value: Long): Long {
|
||||
if (value <= 0) {
|
||||
throw CustomValidationException("timeoutSeconds must be greater than zero")
|
||||
}
|
||||
return value
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
spring:
|
||||
application:
|
||||
name: tle-monitoring-service
|
||||
profiles:
|
||||
default: local
|
||||
config:
|
||||
import: "configserver:"
|
||||
cloud:
|
||||
config:
|
||||
uri: ${CONFIG_SERVER_URI:http://localhost:8888}
|
||||
fail-fast: ${CONFIG_SERVER_FAIL_FAST:true}
|
||||
profile: ${SPRING_CLOUD_CONFIG_PROFILE:${SPRING_PROFILES_ACTIVE:${spring.profiles.default}}}
|
||||
label: ${SPRING_CLOUD_CONFIG_LABEL:dev}
|
||||
|
||||
tle:
|
||||
polling:
|
||||
enabled: ${TLE_POLLING_ENABLED:false}
|
||||
timeout-seconds: ${TLE_POLLING_TIMEOUT_SECONDS:10800}
|
||||
@@ -0,0 +1,9 @@
|
||||
CREATE TABLE IF NOT EXISTS tle (
|
||||
tle_id BIGSERIAL PRIMARY KEY,
|
||||
satellite_id BIGINT NOT NULL,
|
||||
revolution BIGINT NOT NULL,
|
||||
tle TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX idx_tle_satellite_id ON tle(satellite_id);
|
||||
CREATE INDEX idx_tle_revolution ON tle(revolution);
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE tle ADD COLUMN IF NOT EXISTS time_tle TIMESTAMP;
|
||||
ALTER TABLE tle ADD COLUMN IF NOT EXISTS time_reс TIMESTAMP;
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'tle'
|
||||
AND column_name = 'time_reс'
|
||||
) THEN
|
||||
ALTER TABLE tle RENAME COLUMN "time_reс" TO time_rec;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
ALTER TABLE tle ADD COLUMN IF NOT EXISTS time_rec TIMESTAMP;
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package space.nstart.pcp.tle_monitoring_service
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
|
||||
@SpringBootTest
|
||||
class TleMonitoringServiceApplicationTests {
|
||||
|
||||
@Test
|
||||
fun contextLoads() {
|
||||
}
|
||||
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package space.nstart.pcp.tle_monitoring_service.service
|
||||
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.web.reactive.function.client.WebClient
|
||||
import space.nstart.pcp.tle_monitoring_service.configuration.TlePollingProperties
|
||||
|
||||
class CelesTrakServiceTest {
|
||||
|
||||
@Test
|
||||
fun `findExpired does not access satellite source when polling is disabled`() {
|
||||
val pollingSettingsService = TlePollingSettingsService(
|
||||
TlePollingProperties(enabled = false, timeoutSeconds = 30),
|
||||
)
|
||||
val service = CelesTrakService(WebClient.builder().build(), pollingSettingsService)
|
||||
|
||||
val result = service.findExpired().collectList().block().orEmpty()
|
||||
|
||||
assertTrue(result.isEmpty())
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package space.nstart.pcp.tle_monitoring_service.service
|
||||
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.Mockito.mock
|
||||
import org.mockito.Mockito.verify
|
||||
import org.mockito.Mockito.`when`
|
||||
import space.nstart.pcp.tle_monitoring_service.repository.TLERepository
|
||||
|
||||
class SatelliteDeletedServiceTest {
|
||||
|
||||
@Test
|
||||
fun `deleteSatelliteData removes TLE rows`() {
|
||||
val tleRepository = mock(TLERepository::class.java)
|
||||
val service = SatelliteDeletedService(tleRepository)
|
||||
|
||||
`when`(tleRepository.countBySatelliteId(56756L)).thenReturn(4L)
|
||||
`when`(tleRepository.deleteAllBySatelliteId(56756L)).thenReturn(4L)
|
||||
|
||||
val deleted = service.deleteSatelliteData(56756L)
|
||||
|
||||
assertEquals(4L, deleted)
|
||||
verify(tleRepository).countBySatelliteId(56756L)
|
||||
verify(tleRepository).deleteAllBySatelliteId(56756L)
|
||||
}
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
package space.nstart.pcp.tle_monitoring_service.service
|
||||
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertThrows
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
import space.nstart.pcp.tle_monitoring_service.configuration.CustomValidationException
|
||||
import space.nstart.pcp.tle_monitoring_service.configuration.TlePollingProperties
|
||||
import java.time.Instant
|
||||
|
||||
class TlePollingSettingsServiceTest {
|
||||
|
||||
@Test
|
||||
fun `initializes settings from properties`() {
|
||||
val service = TlePollingSettingsService(
|
||||
TlePollingProperties(enabled = false, timeoutSeconds = 45),
|
||||
)
|
||||
|
||||
val settings = service.current()
|
||||
|
||||
assertFalse(settings.enabled)
|
||||
assertEquals(45, settings.timeoutSeconds)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `polling is disabled by default`() {
|
||||
val service = TlePollingSettingsService(TlePollingProperties())
|
||||
|
||||
val settings = service.current()
|
||||
|
||||
assertFalse(settings.enabled)
|
||||
assertFalse(service.shouldStartPolling())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `updates polling settings`() {
|
||||
val service = TlePollingSettingsService(TlePollingProperties())
|
||||
|
||||
val settings = service.update(
|
||||
UpdateTlePollingSettingsRequest(enabled = false, timeoutSeconds = 60),
|
||||
)
|
||||
|
||||
assertFalse(settings.enabled)
|
||||
assertEquals(60, settings.timeoutSeconds)
|
||||
assertFalse(service.shouldStartPolling())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `uses timeout between polling cycles`() {
|
||||
val service = TlePollingSettingsService(
|
||||
TlePollingProperties(enabled = true, timeoutSeconds = 30),
|
||||
)
|
||||
val startedAt = Instant.parse("2026-05-13T10:00:00Z")
|
||||
|
||||
assertTrue(service.shouldStartPolling(startedAt))
|
||||
|
||||
service.markPollingStarted(startedAt)
|
||||
|
||||
assertFalse(service.shouldStartPolling(startedAt.plusSeconds(29)))
|
||||
assertTrue(service.shouldStartPolling(startedAt.plusSeconds(30)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `rejects invalid timeout`() {
|
||||
val service = TlePollingSettingsService(TlePollingProperties())
|
||||
|
||||
assertThrows(CustomValidationException::class.java) {
|
||||
service.update(UpdateTlePollingSettingsRequest(enabled = true, timeoutSeconds = 0))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `starts immediately after polling is enabled`() {
|
||||
val service = TlePollingSettingsService(
|
||||
TlePollingProperties(enabled = true, timeoutSeconds = 30),
|
||||
)
|
||||
val startedAt = Instant.parse("2026-05-13T10:00:00Z")
|
||||
|
||||
service.markPollingStarted(startedAt)
|
||||
service.update(UpdateTlePollingSettingsRequest(enabled = false, timeoutSeconds = 30))
|
||||
service.update(UpdateTlePollingSettingsRequest(enabled = true, timeoutSeconds = 30))
|
||||
|
||||
assertTrue(service.shouldStartPolling(startedAt.plusSeconds(1)))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
spring:
|
||||
application:
|
||||
name: tle-monitoring-service
|
||||
cloud:
|
||||
config:
|
||||
enabled: false
|
||||
kafka:
|
||||
bootstrap-servers: 192.168.60.68:29092
|
||||
consumer:
|
||||
group-id: pcp
|
||||
auto-offset-reset: earliest
|
||||
template:
|
||||
default-topic: pcp.tle
|
||||
datasource:
|
||||
driver-class-name: org.postgresql.Driver
|
||||
url: jdbc:postgresql://192.168.60.68:5432/pcp_tle
|
||||
username: postgres
|
||||
password: password
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: validate
|
||||
properties:
|
||||
hibernate:
|
||||
dialect: org.hibernate.dialect.PostgreSQLDialect
|
||||
jdbc:
|
||||
lob:
|
||||
non_contextual_creation: true
|
||||
flyway:
|
||||
enabled: true
|
||||
baseline-on-migrate: true
|
||||
locations: classpath:db/migration
|
||||
lifecycle.timeout-per-shutdown-phase: 40s
|
||||
jackson:
|
||||
default-property-inclusion: non_null
|
||||
codec:
|
||||
max-in-memory-size: 20MB
|
||||
|
||||
springdoc:
|
||||
swagger-ui:
|
||||
enabled: true
|
||||
layout: BaseLayout
|
||||
path: /swagger/ui
|
||||
api-docs:
|
||||
enabled: true
|
||||
path: /api-docs
|
||||
|
||||
logging:
|
||||
level:
|
||||
.: ERROR
|
||||
file:
|
||||
name: ./logs/application.log
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: "*"
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
info:
|
||||
enabled: true
|
||||
|
||||
settings:
|
||||
complex-mission-service: http://192.168.60.68:7002
|
||||
|
||||
tle:
|
||||
polling:
|
||||
enabled: false
|
||||
timeout-seconds: 10800
|
||||
|
||||
server:
|
||||
port: 7001
|
||||
Reference in New Issue
Block a user