diff --git a/src/main/kotlin/de/fraunhofer/iem/dataprovider/WebSecurityConfiguration.kt b/src/main/kotlin/de/fraunhofer/iem/dataprovider/WebSecurityConfiguration.kt index a7f2a346b3a3606cca9117b5a74fc30c458ea73b..f7db9de0d19bfa042a2dd783aa9a02aaaf85a1d1 100644 --- a/src/main/kotlin/de/fraunhofer/iem/dataprovider/WebSecurityConfiguration.kt +++ b/src/main/kotlin/de/fraunhofer/iem/dataprovider/WebSecurityConfiguration.kt @@ -18,6 +18,10 @@ class SecurityConfiguration { authorizeExchange { authorize("/gitlab/repoChanged", permitAll) authorize("/metrics/latestRuns/{platform}/{id}", permitAll) + authorize("/kpi/{id}", permitAll) + authorize("/kpi/root/repository/{id}", permitAll) + authorize("/repository", permitAll) + authorize("/repository/{id}", permitAll) } csrf { disable() diff --git a/src/main/kotlin/de/fraunhofer/iem/dataprovider/gitlab/GitlabService.kt b/src/main/kotlin/de/fraunhofer/iem/dataprovider/gitlab/GitlabService.kt index f4cad52a6be229d153f630053c9771a2d426e002..0ced35eebc9d8dbc3dd1adcc1d4a45580c1268cd 100644 --- a/src/main/kotlin/de/fraunhofer/iem/dataprovider/gitlab/GitlabService.kt +++ b/src/main/kotlin/de/fraunhofer/iem/dataprovider/gitlab/GitlabService.kt @@ -12,7 +12,7 @@ class GitlabService( ) { suspend fun queryOpenCodeProject(repoId: Long) { - taskManager.addEvent(RepoChangedEvent(repoId = repoId, gitConfiguration = openCodeGitlabConfiguration)) + taskManager.addEvent(RepoChangedEvent(repoId = repoId)) } } \ No newline at end of file diff --git a/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/controller/KPIController.kt b/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/controller/KPIController.kt new file mode 100644 index 0000000000000000000000000000000000000000..1c44c24b3aa5dcd7f66d4796d588ec779a0c8f7e --- /dev/null +++ b/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/controller/KPIController.kt @@ -0,0 +1,35 @@ +package de.fraunhofer.iem.dataprovider.kpi.controller + +import de.fraunhofer.iem.dataprovider.kpi.dto.KPIResultDto +import de.fraunhofer.iem.dataprovider.kpi.service.KPIService +import de.fraunhofer.iem.dataprovider.logger.getLogger +import de.fraunhofer.iem.dataprovider.metrics.* +import de.fraunhofer.iem.dataprovider.toolRun.ToolRunService +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import java.util.UUID + +@RestController +@RequestMapping("/kpi") +class KPIController(private val kpiService: KPIService, private val toolRunService: ToolRunService) { + + private val logger = getLogger(javaClass) + + @GetMapping("/{id}") + suspend fun getKPIByID(@PathVariable id: UUID): KPIResultDto { + logger.info("Get KPI with id $id") + val kpiEntity = this.kpiService.findKPIById(id) ?: throw Exception("KPI with id $id can not be found") + return kpiEntity.toResultDto() + } + + // TODO: Change this to something meaningful + @GetMapping("/root/repository/{id}") + suspend fun getRootKPIByRepositoryId(@PathVariable id: UUID): KPIResultDto { + logger.info("Get root KPI from repository with id $id") + val repository = this.toolRunService.findRepoByID(id) ?: throw Exception("Repository can not be found") + val kpiEntity = this.kpiService.findRootKPIByRepository(repository) ?: throw Exception("Root KPI can not be found") + return kpiEntity.toResultDto() + } +} \ No newline at end of file diff --git a/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/dto/KPIDto.kt b/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/dto/KPIDto.kt new file mode 100644 index 0000000000000000000000000000000000000000..a8a6864d790350de2d82192c59695c017bf1c60f --- /dev/null +++ b/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/dto/KPIDto.kt @@ -0,0 +1,31 @@ +package de.fraunhofer.iem.dataprovider.kpi.dto + +import de.fraunhofer.iem.dataprovider.kpi.model.KPI +import de.fraunhofer.iem.dataprovider.kpi.strategy.KPICalculationStrategy +import java.sql.Timestamp +import java.time.Instant + + +class KPIDto(val name: String, val description: String, val isRoot: Boolean, private val calculationStrategy: KPICalculationStrategy) { + val hierarchyEdges: MutableList<KPIHierarchyEdgeDto> = mutableListOf<KPIHierarchyEdgeDto>() + var value: Int? = null + + fun calculateKPI() { + this.value = calculationStrategy.calculateKPI(this.hierarchyEdges); + } + + fun addChildKPI(kpiDto: KPIDto, weight: Double) { + val hierarchyEdge = KPIHierarchyEdgeDto(this, kpiDto, weight) + this.hierarchyEdges.add(hierarchyEdge) + } + + fun toDbObject(): KPI { + val kpi = KPI() + kpi.name = this.name + kpi.value = this.value + kpi.description = this.description + kpi.isRoot = this.isRoot + kpi.timestamp = Timestamp.from(Instant.now()) + return kpi + } +} \ No newline at end of file diff --git a/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/dto/KPIHierarchyEdgeDto.kt b/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/dto/KPIHierarchyEdgeDto.kt new file mode 100644 index 0000000000000000000000000000000000000000..e6170b9699d2c3442730cc6665ee4e85bd209c73 --- /dev/null +++ b/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/dto/KPIHierarchyEdgeDto.kt @@ -0,0 +1,3 @@ +package de.fraunhofer.iem.dataprovider.kpi.dto + +data class KPIHierarchyEdgeDto (val from: KPIDto, val to: KPIDto, val weight: Double) \ No newline at end of file diff --git a/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/dto/KPIResultChildDto.kt b/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/dto/KPIResultChildDto.kt new file mode 100644 index 0000000000000000000000000000000000000000..1d68a4c7a6740c919fa17a31782987478f6f5b79 --- /dev/null +++ b/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/dto/KPIResultChildDto.kt @@ -0,0 +1,5 @@ +package de.fraunhofer.iem.dataprovider.kpi.dto + +import java.util.* + +data class KPIResultChildDto(val id: UUID, val weight: Double) \ No newline at end of file diff --git a/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/dto/KPIResultDto.kt b/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/dto/KPIResultDto.kt new file mode 100644 index 0000000000000000000000000000000000000000..a8ddfd72e0c90df960b239b442e2d828a42dc28f --- /dev/null +++ b/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/dto/KPIResultDto.kt @@ -0,0 +1,5 @@ +package de.fraunhofer.iem.dataprovider.kpi.dto + +import java.util.* + +data class KPIResultDto(val id: UUID, val name: String, val description: String, val isRoot: Boolean, val children: List<KPIResultChildDto>, val repositoryId: UUID) {} \ No newline at end of file diff --git a/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/model/KPI.kt b/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/model/KPI.kt new file mode 100644 index 0000000000000000000000000000000000000000..f53c1c27ec11eb9327f1ede4a2aea665fe15a3da --- /dev/null +++ b/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/model/KPI.kt @@ -0,0 +1,70 @@ +package de.fraunhofer.iem.dataprovider.kpi.model + +import de.fraunhofer.iem.dataprovider.kpi.dto.KPIResultChildDto +import de.fraunhofer.iem.dataprovider.kpi.dto.KPIResultDto +import de.fraunhofer.iem.dataprovider.toolRun.model.Repository +import jakarta.persistence.* +import org.hibernate.annotations.JdbcTypeCode +import org.hibernate.type.SqlTypes +import java.sql.Timestamp +import java.util.* + +@Entity +@Table(name = "kpi") +class KPI { + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + @Column(name = "id", nullable = false) + var id: UUID? = null + + @ManyToOne + @JoinColumn(name = "repository_id") + var repository: Repository? = null + + @OneToMany(mappedBy = "from", cascade = [CascadeType.MERGE, CascadeType.REMOVE], orphanRemoval = true) + var hierarchyEdges: MutableSet<KPIHierarchyEdge> = mutableSetOf() + + @Column(name = "value") + var value: Int? = null + + @Column(name = "isRoot") + var isRoot: Boolean? = null + + @Column(name = "name") + var name: String? = null + + @Column(name = "description") + var description: String? = null + + @JdbcTypeCode(SqlTypes.TIMESTAMP) + @Column(name = "timestamp") + var timestamp: Timestamp? = null + + fun addHierarchyEdges(kpiHierarchyEdges: Collection<KPIHierarchyEdge>) { + kpiHierarchyEdges.forEach { + this.addHierarchyEdge(it) + } + } + + fun addHierarchyEdge(hierarchyEdge: KPIHierarchyEdge) { + hierarchyEdge.from = this + this.hierarchyEdges.add(hierarchyEdge) + } + + fun toResultDto(): KPIResultDto { + val childrenList = mutableListOf<KPIResultChildDto>() + for (edge in this.hierarchyEdges) { + val id = edge.to!!.id!! + val weight = edge.weight!! + childrenList.add(KPIResultChildDto(id, weight)) + } + return KPIResultDto( + this.id!!, + this.name!!, + this.description!!, + this.isRoot!!, + childrenList, + this.repository!!.id!! + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/model/KPIHierarchyEdge.kt b/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/model/KPIHierarchyEdge.kt new file mode 100644 index 0000000000000000000000000000000000000000..6994560f93a2b26b438c77aec9e3aa5c1ea88bd9 --- /dev/null +++ b/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/model/KPIHierarchyEdge.kt @@ -0,0 +1,24 @@ +package de.fraunhofer.iem.dataprovider.kpi.model + +import jakarta.persistence.* +import java.util.* + +@Entity +@Table(name = "kpi_hierarchy_edge") +class KPIHierarchyEdge { + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + @Column(name = "id", nullable = false) + var id: UUID? = null + + @ManyToOne(cascade = [CascadeType.MERGE]) + @JoinColumn(name = "from_id") + var from: KPI? = null + + @ManyToOne(cascade = [CascadeType.MERGE]) + @JoinColumn(name = "to_id") + var to: KPI? = null + + @Column(name = "weight") + var weight: Double? = null +} \ No newline at end of file diff --git a/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/repository/KPIHierarchyEdgeRepository.kt b/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/repository/KPIHierarchyEdgeRepository.kt new file mode 100644 index 0000000000000000000000000000000000000000..5d455ef31b09b1b26aa62cf7baf8da69ecd64166 --- /dev/null +++ b/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/repository/KPIHierarchyEdgeRepository.kt @@ -0,0 +1,7 @@ +package de.fraunhofer.iem.dataprovider.kpi.repository + +import de.fraunhofer.iem.dataprovider.kpi.model.KPIHierarchyEdge +import org.springframework.data.jpa.repository.JpaRepository +import java.util.* + +interface KPIHierarchyEdgeRepository: JpaRepository<KPIHierarchyEdge, UUID> {} diff --git a/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/repository/KPIRepository.kt b/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/repository/KPIRepository.kt new file mode 100644 index 0000000000000000000000000000000000000000..3967d7caab516def4a6be42cb894c474dc620715 --- /dev/null +++ b/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/repository/KPIRepository.kt @@ -0,0 +1,11 @@ +package de.fraunhofer.iem.dataprovider.kpi.repository + +import de.fraunhofer.iem.dataprovider.kpi.model.KPI +import org.springframework.data.jpa.repository.JpaRepository +import java.util.* + +interface KPIRepository : JpaRepository<KPI, UUID> { + fun findByRepository_Id(id: UUID): List<KPI> + + fun findFirstByRepository_IdAndIsRootTrue(id: UUID): Optional<KPI> +} \ No newline at end of file diff --git a/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/service/KPIService.kt b/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/service/KPIService.kt new file mode 100644 index 0000000000000000000000000000000000000000..e6ab6b0f3deb6d8b3398e12ab32e316bac0448c3 --- /dev/null +++ b/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/service/KPIService.kt @@ -0,0 +1,106 @@ +package de.fraunhofer.iem.dataprovider.kpi.service + +import de.fraunhofer.iem.dataprovider.kpi.dto.KPIDto +import de.fraunhofer.iem.dataprovider.kpi.model.KPI +import de.fraunhofer.iem.dataprovider.kpi.model.KPIHierarchyEdge +import de.fraunhofer.iem.dataprovider.kpi.repository.KPIHierarchyEdgeRepository +import de.fraunhofer.iem.dataprovider.kpi.repository.KPIRepository +import de.fraunhofer.iem.dataprovider.logger.getLogger +import de.fraunhofer.iem.dataprovider.toolRun.model.Repository +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import java.util.* + +@Service +class KPIService(private val kpiRepository: KPIRepository, private val kpiHierarchyEdgeRepository: KPIHierarchyEdgeRepository) { + + private val logger = getLogger(javaClass) + + private fun calculateKPIsRecursively(kpi: KPIDto, visited: MutableSet<KPIDto>) { + // Check if the KPI has already been visited + if (visited.contains(kpi)) { + return + } + + // Check if the KPI has child KPIs + if (kpi.hierarchyEdges.isEmpty()) { + // Leaf node, calculate the KPI value + kpi.calculateKPI() + visited.add(kpi) + return + } + + // Recursively calculate child KPIs first + for (childEdge in kpi.hierarchyEdges) { + calculateKPIsRecursively(childEdge.to, visited) + } + + // Calculate the KPI value after processing child KPIs + kpi.calculateKPI() + visited.add(kpi) + } + + fun calculateKPIs(rootKPI: KPIDto) { + val visited: MutableSet<KPIDto> = mutableSetOf() + calculateKPIsRecursively(rootKPI, visited) + } + + fun storeAndPurgeOld(repository: Repository, rootKPI: KPIDto) { + purgeAllKPIs(repository) + val kpiDtoToEntityMapping: MutableMap<KPIDto, KPI> = mutableMapOf() + storeKPI(repository, rootKPI, kpiDtoToEntityMapping) + } + + private fun storeKPI(repository: Repository, kpi: KPIDto, kpiDtoToEntityMapping: MutableMap<KPIDto, KPI>) { + // already visited + if (kpiDtoToEntityMapping.contains(kpi)) { + return + } + + if (kpi.hierarchyEdges.isEmpty()) { + // leaf node + val kpiEntity = kpi.toDbObject() + kpiEntity.repository = repository + + kpiDtoToEntityMapping[kpi] = kpiEntity + kpiRepository.save(kpiEntity) + logger.info("Storing leaf node ${kpi.name}") + return + } + + for (childEdge in kpi.hierarchyEdges) { + storeKPI(repository, childEdge.to, kpiDtoToEntityMapping) + } + + val kpiEntity = kpi.toDbObject() + kpiEntity.repository = repository + kpiDtoToEntityMapping[kpi] = kpiEntity + kpiRepository.save(kpiEntity) + logger.info("Storing node ${kpi.name}") + for (hierarchyEdge in kpi.hierarchyEdges) { + val hierarchyEdgeEntity = KPIHierarchyEdge() + hierarchyEdgeEntity.from = kpiEntity + hierarchyEdgeEntity.to = kpiDtoToEntityMapping[hierarchyEdge.to] + hierarchyEdgeEntity.weight = hierarchyEdge.weight + kpiHierarchyEdgeRepository.save(hierarchyEdgeEntity) + logger.info("Storing edge ${hierarchyEdge.weight}: From ${hierarchyEdge.from.name} to ${hierarchyEdge.to.name}") + } + } + + private fun purgeAllKPIs(repository: Repository) { + val kpis: List<KPI> = kpiRepository.findByRepository_Id(repository.id!!) + kpiRepository.deleteAll(kpis) + } + + fun findKPIById(id: UUID): KPI? { + return kpiRepository.findByIdOrNull(id) + } + + fun findRootKPIByRepository(repository: Repository): KPI? { + val kpi = kpiRepository.findFirstByRepository_IdAndIsRootTrue(repository.id!!) + if (kpi.isEmpty) { + return null + } + return kpi.get() + } +} \ No newline at end of file diff --git a/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/strategy/AggregationKPICalculationStrategy.kt b/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/strategy/AggregationKPICalculationStrategy.kt new file mode 100644 index 0000000000000000000000000000000000000000..394778a6feb7c2fe2014292f2e1caa098395628d --- /dev/null +++ b/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/strategy/AggregationKPICalculationStrategy.kt @@ -0,0 +1,16 @@ +package de.fraunhofer.iem.dataprovider.kpi.strategy + +import de.fraunhofer.iem.dataprovider.kpi.dto.KPIHierarchyEdgeDto + +class AggregationKPICalculationStrategy: KPICalculationStrategy { + override fun calculateKPI(children: List<KPIHierarchyEdgeDto>): Int { + var aggregate = 0; + for (child in children) { + if (child.to.value != null) { + val childValue = child.to.value!!; + aggregate += (childValue.toFloat() * child.weight).toInt(); + } + } + return aggregate; + } +} \ No newline at end of file diff --git a/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/strategy/KPICalculationStrategy.kt b/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/strategy/KPICalculationStrategy.kt new file mode 100644 index 0000000000000000000000000000000000000000..1545b7f3a2c1cafb5ea29e1c7239be64243abeec --- /dev/null +++ b/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/strategy/KPICalculationStrategy.kt @@ -0,0 +1,8 @@ +package de.fraunhofer.iem.dataprovider.kpi.strategy + +import de.fraunhofer.iem.dataprovider.kpi.dto.KPIHierarchyEdgeDto + +interface KPICalculationStrategy { + fun calculateKPI(children: List<KPIHierarchyEdgeDto>): Int; +} + diff --git a/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/strategy/RatioKPICalculationStrategy.kt b/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/strategy/RatioKPICalculationStrategy.kt new file mode 100644 index 0000000000000000000000000000000000000000..ae66607cd4aa1b962fcfef71dfe21c730ff92bda --- /dev/null +++ b/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/strategy/RatioKPICalculationStrategy.kt @@ -0,0 +1,17 @@ +package de.fraunhofer.iem.dataprovider.kpi.strategy + +import de.fraunhofer.iem.dataprovider.kpi.dto.KPIHierarchyEdgeDto + +class RatioKPICalculationStrategy(): KPICalculationStrategy { + override fun calculateKPI(children: List<KPIHierarchyEdgeDto>): Int { + if (children.size != 2) { + throw Exception("Requires exactly two children") + } + val firstValue = children[0].to.value + val secondValue = children[1].to.value + if (firstValue!! >= secondValue!!) { + return ((secondValue / firstValue) * 100).toInt(); + } + return ((firstValue / secondValue) * 100).toInt(); + } +} \ No newline at end of file diff --git a/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/strategy/RawValueKPICalculationStrategy.kt b/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/strategy/RawValueKPICalculationStrategy.kt new file mode 100644 index 0000000000000000000000000000000000000000..d81b0fbcd6d4dbf2301c483616ed2b564826a62a --- /dev/null +++ b/src/main/kotlin/de/fraunhofer/iem/dataprovider/kpi/strategy/RawValueKPICalculationStrategy.kt @@ -0,0 +1,9 @@ +package de.fraunhofer.iem.dataprovider.kpi.strategy + +import de.fraunhofer.iem.dataprovider.kpi.dto.KPIHierarchyEdgeDto + +class RawValueKPICalculationStrategy(val value: Int): KPICalculationStrategy { + override fun calculateKPI(children: List<KPIHierarchyEdgeDto>): Int { + return this.value; + } +} \ No newline at end of file diff --git a/src/main/kotlin/de/fraunhofer/iem/dataprovider/metrics/MetricsController.kt b/src/main/kotlin/de/fraunhofer/iem/dataprovider/metrics/MetricsController.kt index a9926f043b6cb3b73277ae070cd0ed0217576bdc..de94d4a75d6a8659dc3c5807324df03bc9ec46bc 100644 --- a/src/main/kotlin/de/fraunhofer/iem/dataprovider/metrics/MetricsController.kt +++ b/src/main/kotlin/de/fraunhofer/iem/dataprovider/metrics/MetricsController.kt @@ -35,6 +35,5 @@ class MetricsController(private val toolRunService: ToolRunService) { ) } return ToolResultsDto(repoDto, trDto) - } } \ No newline at end of file diff --git a/src/main/kotlin/de/fraunhofer/iem/dataprovider/repository/controller/RepositoryController.kt b/src/main/kotlin/de/fraunhofer/iem/dataprovider/repository/controller/RepositoryController.kt new file mode 100644 index 0000000000000000000000000000000000000000..42b68c67410b9c509bee2d593b895319241b80e2 --- /dev/null +++ b/src/main/kotlin/de/fraunhofer/iem/dataprovider/repository/controller/RepositoryController.kt @@ -0,0 +1,32 @@ +package de.fraunhofer.iem.dataprovider.repository.controller + +import de.fraunhofer.iem.dataprovider.logger.getLogger +import de.fraunhofer.iem.dataprovider.metrics.* +import de.fraunhofer.iem.dataprovider.repository.dto.RepositoryDto +import de.fraunhofer.iem.dataprovider.toolRun.ToolRunService +import de.fraunhofer.iem.dataprovider.toolRun.model.Repository +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import java.util.UUID + +@RestController +@RequestMapping("/repository") +class RepositoryController(private val toolRunService: ToolRunService) { + + private val logger = getLogger(javaClass) + + @GetMapping() + suspend fun getAllRepositories(): List<RepositoryDto> { + logger.info("Get all repositories") + return toolRunService.getAllRepositories().map { repository: Repository -> RepositoryDto(repository.id!!, repository.name!!) } + } + + @GetMapping("/{id}") + suspend fun getRepositoryById(@PathVariable id: UUID): RepositoryDto { + logger.info("Get repository with id $id") + val repositoryEntity = this.toolRunService.findRepoByID(id) ?: throw Exception("Repository can not be found") + return RepositoryDto(repositoryEntity.id!!, repositoryEntity.name!!) + } +} \ No newline at end of file diff --git a/src/main/kotlin/de/fraunhofer/iem/dataprovider/repository/dto/RepositoryDto.kt b/src/main/kotlin/de/fraunhofer/iem/dataprovider/repository/dto/RepositoryDto.kt new file mode 100644 index 0000000000000000000000000000000000000000..b01fef603dd5a030224f12033f4477646dd8aa72 --- /dev/null +++ b/src/main/kotlin/de/fraunhofer/iem/dataprovider/repository/dto/RepositoryDto.kt @@ -0,0 +1,5 @@ +package de.fraunhofer.iem.dataprovider.repository.dto + +import java.util.* + +data class RepositoryDto(val id: UUID, val name: String){} \ No newline at end of file diff --git a/src/main/kotlin/de/fraunhofer/iem/dataprovider/taskManager/TaskManager.kt b/src/main/kotlin/de/fraunhofer/iem/dataprovider/taskManager/TaskManager.kt index 41507f32b9642c0befadc9de0cf60f1012e515f3..aab943f1ac4ca43f926012f53b2cd38d74902fb9 100644 --- a/src/main/kotlin/de/fraunhofer/iem/dataprovider/taskManager/TaskManager.kt +++ b/src/main/kotlin/de/fraunhofer/iem/dataprovider/taskManager/TaskManager.kt @@ -1,5 +1,7 @@ package de.fraunhofer.iem.dataprovider.taskManager +import de.fraunhofer.iem.dataprovider.gitlab.OpenCodeGitlabConfiguration +import de.fraunhofer.iem.dataprovider.kpi.service.KPIService import de.fraunhofer.iem.dataprovider.logger.getLogger import de.fraunhofer.iem.dataprovider.taskManager.model.* import de.fraunhofer.iem.dataprovider.taskManager.tasks.* @@ -24,8 +26,10 @@ import java.util.* */ @Component class TaskManager( - private val config: Config, - private val toolRunService: ToolRunService + private val config: Config, + private val openCodeGitlabConfiguration: OpenCodeGitlabConfiguration, + private val toolRunService: ToolRunService, + private val kpiService: KPIService ) { // The used default dispatcher is ok for CPU-bound workloads. However, @@ -81,7 +85,7 @@ class TaskManager( ioWorker.addTask( GetGitlabProjectTask( event.repoId, - event.gitConfiguration, + openCodeGitlabConfiguration, ::addEvent, toolRunService ) @@ -145,16 +149,20 @@ class TaskManager( } is SarifProcessGroupDone -> { + logger.info("Done with Sarif Task") val eventGroup = dependentEvents[event.groupId] - if (eventGroup != null) { eventGroup.remove(event.taskId) if (eventGroup.isEmpty()) { - worker.addTask(MetricsTask(event.repoId, toolRunService, ::addEvent)) + logger.info("Adding repository details task") + worker.addTask(GetRepositoryDetailsTask(event.repoId, openCodeGitlabConfiguration, toolRunService, ::addEvent)) } } } + is GetRepositoryDetailsDone -> { + worker.addTask(MetricsTask(event.repoId, toolRunService, kpiService, ::addEvent)) + } else -> { logger.info("Received event without special handling associated $event") diff --git a/src/main/kotlin/de/fraunhofer/iem/dataprovider/taskManager/model/Event.kt b/src/main/kotlin/de/fraunhofer/iem/dataprovider/taskManager/model/Event.kt index 7d3eaade44f873907fa74c3dfe4f41e4a2c675cc..c05208f79850642cc03a79bbe60aac0b2d4642ae 100644 --- a/src/main/kotlin/de/fraunhofer/iem/dataprovider/taskManager/model/Event.kt +++ b/src/main/kotlin/de/fraunhofer/iem/dataprovider/taskManager/model/Event.kt @@ -1,12 +1,12 @@ package de.fraunhofer.iem.dataprovider.taskManager.model -import de.fraunhofer.iem.dataprovider.gitlab.GitConfiguration import de.fraunhofer.iem.dataprovider.taskManager.tasks.GitRepository +import de.fraunhofer.iem.dataprovider.toolRun.model.RepositoryDetails import de.fraunhofer.iem.dataprovider.toolRun.model.Sarif import java.util.* sealed class Event -class RepoChangedEvent(val gitConfiguration: GitConfiguration, val repoId: Long) : Event() +class RepoChangedEvent(val repoId: Long) : Event() sealed class TaskDone : Event() { abstract val taskId: UUID @@ -18,3 +18,5 @@ class GetGitlabProjectDone(override val taskId: UUID, val repoId: UUID, val gitR class SarifProcessDone(override val taskId: UUID, val repoId: UUID, val sarif: Sarif) : TaskDone() class SarifProcessGroupDone(override val taskId: UUID, val repoId: UUID, val groupId: UUID, val sarif: Sarif) : TaskDone() + +class GetRepositoryDetailsDone(override val taskId: UUID, val repoId: UUID, val repositoryDetailsEntity: RepositoryDetails) : TaskDone() diff --git a/src/main/kotlin/de/fraunhofer/iem/dataprovider/taskManager/tasks/GetRepositoryDetailsTask.kt b/src/main/kotlin/de/fraunhofer/iem/dataprovider/taskManager/tasks/GetRepositoryDetailsTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..e85c940c84fa49339dff39671f65865e7f539e57 --- /dev/null +++ b/src/main/kotlin/de/fraunhofer/iem/dataprovider/taskManager/tasks/GetRepositoryDetailsTask.kt @@ -0,0 +1,65 @@ +package de.fraunhofer.iem.dataprovider.taskManager.tasks + +import de.fraunhofer.iem.dataprovider.gitlab.GitConfiguration +import de.fraunhofer.iem.dataprovider.taskManager.model.Event +import de.fraunhofer.iem.dataprovider.taskManager.model.GetRepositoryDetailsDone +import de.fraunhofer.iem.dataprovider.toolRun.ToolRunService +import de.fraunhofer.iem.dataprovider.toolRun.model.Repository +import org.gitlab4j.api.GitLabApi +import org.gitlab4j.api.models.Commit +import org.gitlab4j.api.models.Project +import java.util.UUID + +class GetRepositoryDetailsTask( + private val repoId: UUID, + private val gitlabConfiguration: GitConfiguration, + private val toolRunService: ToolRunService, + override val responseChannel: suspend (event: Event) -> Unit, +) : Task() { + private val gitlabApi: GitLabApi = GitLabApi(gitlabConfiguration.host, gitlabConfiguration.accessToken) + + override suspend fun execute() { + val repositoryEntity = toolRunService.findRepoByID(repoId); + logger.info(repositoryEntity.toString()) + if (repositoryEntity != null) { + val project = gitlabApi.projectApi.getProject(repositoryEntity.repoId) + // Note: We only take commits from the default branch + val commits = gitlabApi.commitsApi.getCommits(project.id, project.defaultBranch, ".") + + val numberOfCommits = commits.count() + val numberOfSignedCommits = getNumberOfSignedCommits(repositoryEntity, commits) + val isDefaultBranchProtected = isDefaultBranchProtected(repositoryEntity, project) + val repositoryDetailsDto = RepositoryDetailsDto(repositoryEntity, numberOfCommits, numberOfSignedCommits, isDefaultBranchProtected) + logger.info("Collected repository details from $repoId (${repositoryDetailsDto}) successfully") + val repositoryDetailsEntity = toolRunService.createRepositoryDetails(repositoryDetailsDto); + logger.info("Stored repository details for $repoId (${repositoryDetailsEntity}) successfully") + responseChannel(GetRepositoryDetailsDone(taskID, repoId, repositoryDetailsEntity)) + } else { + logger.error("Repository $repoId can not be found in the database") + } + } + + // TODO: This should probably live somewhere else and encapsulate the logic + private fun getNumberOfSignedCommits(repositoryEntity: Repository, commits: List<Commit>): Int { + var numberOfSignedCommits = 0 + for (commit in commits) { + val signature = gitlabApi.commitsApi.getOptionalGpgSignature(repositoryEntity.repoId, commit.id) + if (signature !== null) { + numberOfSignedCommits++ + } + } + return numberOfSignedCommits + } + + // TODO: This should probably live somewhere else and encapsulate the logic + private fun isDefaultBranchProtected(repositoryEntity: Repository, project: Project): Boolean { + return try { + val defaultBranchName = project.defaultBranch; + val branch = gitlabApi.repositoryApi.getBranch(repositoryEntity.repoId, defaultBranchName); + branch.protected + } catch (e: Exception) { + // in theory, error probably happens if branch can't be found. In this case we default to false + false; + } + } +} diff --git a/src/main/kotlin/de/fraunhofer/iem/dataprovider/taskManager/tasks/MetricsTask.kt b/src/main/kotlin/de/fraunhofer/iem/dataprovider/taskManager/tasks/MetricsTask.kt index 9b583dd61c0524d562daa948e62796aaaaf32c82..6d75ef756444d157d37a5cfc0e1ba9283f234756 100644 --- a/src/main/kotlin/de/fraunhofer/iem/dataprovider/taskManager/tasks/MetricsTask.kt +++ b/src/main/kotlin/de/fraunhofer/iem/dataprovider/taskManager/tasks/MetricsTask.kt @@ -1,17 +1,52 @@ package de.fraunhofer.iem.dataprovider.taskManager.tasks +import de.fraunhofer.iem.dataprovider.kpi.dto.KPIDto +import de.fraunhofer.iem.dataprovider.kpi.service.KPIService import de.fraunhofer.iem.dataprovider.taskManager.model.Event +import de.fraunhofer.iem.dataprovider.kpi.strategy.AggregationKPICalculationStrategy +import de.fraunhofer.iem.dataprovider.kpi.strategy.RatioKPICalculationStrategy +import de.fraunhofer.iem.dataprovider.kpi.strategy.RawValueKPICalculationStrategy import de.fraunhofer.iem.dataprovider.toolRun.ToolRunService +import de.fraunhofer.iem.dataprovider.toolRun.model.RepositoryDetails import java.util.* class MetricsTask( val repoId: UUID, private val toolRunService: ToolRunService, - + private val kpiService: KPIService, override val responseChannel: suspend (task: Event) -> Unit ) : Task() { override suspend fun execute() { logger.info("Starting metrics task for repoId $repoId") - toolRunService.getLatestToolRunsForRepo(repoId) + val repository = toolRunService.findRepoByID(repoId); + val repositoryDetails = toolRunService.getLatestRepositoryDetailsByRepositoryId(repoId); + if (repositoryDetails != null && repository != null) { + val rootKPI = generateKPITree(repositoryDetails) + kpiService.calculateKPIs(rootKPI) + kpiService.storeAndPurgeOld(repository, rootKPI) + } + } + + private fun generateKPITree(repositoryDetails: RepositoryDetails): KPIDto { + // lowest level leaves + val numberOfCommitsKPI = KPIDto( "Number of commits", "Overall transparency score between 0-100 :-)", false, RawValueKPICalculationStrategy(repositoryDetails.numberOfCommits!!)) + val numberOfSignedCommitsKPI = KPIDto("Number of signed commits", "Overall transparency score between 0-100 :-)", false, RawValueKPICalculationStrategy(repositoryDetails.numberOfSignedCommits!!)) + val isDefaultBranchProtectedKPI = KPIDto( "Is Default Branch Protected", "Is the default branch protected?", false, RawValueKPICalculationStrategy(if (repositoryDetails.isDefaultBranchProtected == true) 100 else 0)) + + val signedCommitsRatioKPI = KPIDto( "Signed Commit Ratio", "Ratio between signed and all commits", false, RatioKPICalculationStrategy()) + signedCommitsRatioKPI.addChildKPI(numberOfCommitsKPI, 1.0) + signedCommitsRatioKPI.addChildKPI(numberOfSignedCommitsKPI, 1.0) + + // second level + val securityKPI = KPIDto( "Security score", "Overall project security score between 0-100 :-)", false, AggregationKPICalculationStrategy()) + val transparencyKPI = KPIDto( "Transparency score", "Overall transparency score between 0-100 :-)", false, AggregationKPICalculationStrategy()) + securityKPI.addChildKPI(isDefaultBranchProtectedKPI, 0.5) + securityKPI.addChildKPI(signedCommitsRatioKPI, 0.5) + transparencyKPI.addChildKPI(signedCommitsRatioKPI, 1.0) + + val rootKPI = KPIDto( "Project score", "Overall project score between 0-100 :-)", true, AggregationKPICalculationStrategy()) + rootKPI.addChildKPI(securityKPI, 0.5) + rootKPI.addChildKPI(transparencyKPI, 0.5) + return rootKPI } } diff --git a/src/main/kotlin/de/fraunhofer/iem/dataprovider/taskManager/tasks/RepositoryDetailsDto.kt b/src/main/kotlin/de/fraunhofer/iem/dataprovider/taskManager/tasks/RepositoryDetailsDto.kt new file mode 100644 index 0000000000000000000000000000000000000000..4c24fec643f567a9ae83cf37171e001ddbee389a --- /dev/null +++ b/src/main/kotlin/de/fraunhofer/iem/dataprovider/taskManager/tasks/RepositoryDetailsDto.kt @@ -0,0 +1,18 @@ +package de.fraunhofer.iem.dataprovider.taskManager.tasks + +import de.fraunhofer.iem.dataprovider.toolRun.model.Repository +import de.fraunhofer.iem.dataprovider.toolRun.model.RepositoryDetails +import java.sql.Timestamp +import java.time.Instant + +data class RepositoryDetailsDto(val repository: Repository, val numberOfCommits: Int, val numberOfSignedCommits: Int, val isDefaultBranchProtected: Boolean) + +fun RepositoryDetailsDto.toDbObject(): RepositoryDetails { + val repositoryDetails = RepositoryDetails() + repositoryDetails.repository = this.repository + repositoryDetails.timestamp = Timestamp.from(Instant.now()) + repositoryDetails.numberOfCommits = this.numberOfCommits + repositoryDetails.numberOfSignedCommits = this.numberOfSignedCommits + repositoryDetails.isDefaultBranchProtected = this.isDefaultBranchProtected + return repositoryDetails +} \ No newline at end of file diff --git a/src/main/kotlin/de/fraunhofer/iem/dataprovider/toolRun/RepositoryDetailsRepository.kt b/src/main/kotlin/de/fraunhofer/iem/dataprovider/toolRun/RepositoryDetailsRepository.kt new file mode 100644 index 0000000000000000000000000000000000000000..a13b4997cbe95c8202fb0487aeaf505544167548 --- /dev/null +++ b/src/main/kotlin/de/fraunhofer/iem/dataprovider/toolRun/RepositoryDetailsRepository.kt @@ -0,0 +1,11 @@ +package de.fraunhofer.iem.dataprovider.toolRun + +import de.fraunhofer.iem.dataprovider.toolRun.model.RepositoryDetails +import org.springframework.data.domain.Sort +import org.springframework.data.jpa.repository.JpaRepository +import java.util.* + + +interface RepositoryDetailsRepository : JpaRepository<RepositoryDetails, UUID> { + fun findFirstByRepository_IdOrderByTimestampDesc(id: UUID): Optional<RepositoryDetails> +} diff --git a/src/main/kotlin/de/fraunhofer/iem/dataprovider/toolRun/ToolRunService.kt b/src/main/kotlin/de/fraunhofer/iem/dataprovider/toolRun/ToolRunService.kt index 2e8808b9dadf4c2dec73681937ee12580cb79399..086578a4474204ed9be25f2ac0e878d10e3cddd0 100644 --- a/src/main/kotlin/de/fraunhofer/iem/dataprovider/toolRun/ToolRunService.kt +++ b/src/main/kotlin/de/fraunhofer/iem/dataprovider/toolRun/ToolRunService.kt @@ -3,6 +3,7 @@ package de.fraunhofer.iem.dataprovider.toolRun import de.fraunhofer.iem.dataprovider.gitlab.Platform import de.fraunhofer.iem.dataprovider.logger.getLogger import de.fraunhofer.iem.dataprovider.taskManager.tasks.GitRepository +import de.fraunhofer.iem.dataprovider.taskManager.tasks.RepositoryDetailsDto import de.fraunhofer.iem.dataprovider.taskManager.tasks.toDbObject import de.fraunhofer.iem.dataprovider.toolRun.model.* import org.springframework.stereotype.Service @@ -11,6 +12,7 @@ import java.util.* @Service class ToolRunService( private val repositoryRepository: RepositoryRepository, + private val repositoryDetailsRepository: RepositoryDetailsRepository, private val toolRunRepository: ToolRunRepository, private val toolRepository: ToolRepository, private val ruleRepository: RuleRepository @@ -121,4 +123,38 @@ class ToolRunService( } return repo } + + // TODO: We should decide if we want to return optionals (null if not found) or throw exceptions or ... + // TODO: Must be consistent across all methods + fun findRepoByID(id: UUID): Repository? { + val repository = repositoryRepository.findById(id) + if (repository.isEmpty) { + return null + } + return repository.get() + } + + fun createRepositoryDetails(repositoryDetailsDto: RepositoryDetailsDto): RepositoryDetails { + return repositoryDetailsRepository.save(repositoryDetailsDto.toDbObject()) + } + + fun getLatestRepositoryDetailsByRepositoryId(repositoryId: UUID): RepositoryDetails? { + val repositoryDetails = repositoryDetailsRepository.findFirstByRepository_IdOrderByTimestampDesc(repositoryId); + if (repositoryDetails.isEmpty) { + return null + } + return repositoryDetails.get(); + } + + fun getRepositoryDetailsById(id: UUID): RepositoryDetails? { + val repositoryDetails = repositoryDetailsRepository.findById(id); + if (repositoryDetails.isEmpty) { + return null + } + return repositoryDetails.get() + } + + fun getAllRepositories(): List<Repository> { + return repositoryRepository.findAll() + } } diff --git a/src/main/kotlin/de/fraunhofer/iem/dataprovider/toolRun/model/RepositoryDetails.kt b/src/main/kotlin/de/fraunhofer/iem/dataprovider/toolRun/model/RepositoryDetails.kt new file mode 100644 index 0000000000000000000000000000000000000000..52a0fe37ae638b4f68f413c49d544c46a369470b --- /dev/null +++ b/src/main/kotlin/de/fraunhofer/iem/dataprovider/toolRun/model/RepositoryDetails.kt @@ -0,0 +1,32 @@ +package de.fraunhofer.iem.dataprovider.toolRun.model + +import jakarta.persistence.* +import org.hibernate.annotations.JdbcTypeCode +import org.hibernate.type.SqlTypes +import java.sql.Timestamp +import java.util.* + +@Entity +@Table(name = "repository_details") +class RepositoryDetails { + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + @Column(name = "id", nullable = false) + var id: UUID? = null + + @ManyToOne(fetch = FetchType.EAGER) + var repository: Repository? = null + + @Column(name = "number_of_commits") + var numberOfCommits: Int? = null + + @Column(name = "number_of_signed_commits") + var numberOfSignedCommits: Int? = null + + @Column(name = "is_default_branch_protected") + var isDefaultBranchProtected: Boolean? = null + + @JdbcTypeCode(SqlTypes.TIMESTAMP) + @Column(name = "timestamp") + var timestamp: Timestamp? = null +} \ No newline at end of file