diff --git a/plugins/package-curation-providers/spring/build.gradle.kts b/plugins/package-curation-providers/spring/build.gradle.kts new file mode 100644 index 0000000000000..eb751ab71d72e --- /dev/null +++ b/plugins/package-curation-providers/spring/build.gradle.kts @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2025 The ORT Project Authors (see ) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +plugins { + // Apply precompiled plugins. + id("ort-plugin-conventions") +} + +dependencies { + api(projects.plugins.packageCurationProviders.packageCurationProviderApi) + + ksp(projects.plugins.packageCurationProviders.packageCurationProviderApi) + + implementation(projects.model) + implementation(projects.utils.commonUtils) + implementation(projects.utils.ortUtils) + + implementation(libs.kotlinx.serialization.json) +} diff --git a/plugins/package-curation-providers/spring/src/main/kotlin/SpringPackageCurationProvider.kt b/plugins/package-curation-providers/spring/src/main/kotlin/SpringPackageCurationProvider.kt new file mode 100644 index 0000000000000..c90db05ad1d9e --- /dev/null +++ b/plugins/package-curation-providers/spring/src/main/kotlin/SpringPackageCurationProvider.kt @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2025 The ORT Project Authors (see ) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +package org.ossreviewtoolkit.plugins.packagecurationproviders.spring + +import org.ossreviewtoolkit.model.Package +import org.ossreviewtoolkit.model.PackageCuration +import org.ossreviewtoolkit.model.PackageCurationData +import org.ossreviewtoolkit.model.VcsInfoCurationData +import org.ossreviewtoolkit.plugins.api.OrtPlugin +import org.ossreviewtoolkit.plugins.api.PluginDescriptor +import org.ossreviewtoolkit.plugins.packagecurationproviders.api.PackageCurationProvider +import org.ossreviewtoolkit.plugins.packagecurationproviders.api.PackageCurationProviderFactory + +/** + * A [PackageCurationProvider] that provides [PackageCuration]s for Spring (https://spring.io/) packages. + */ +@OrtPlugin( + displayName = "Spring", + description = "A package curation provider for Spring packages.", + factory = PackageCurationProviderFactory::class +) +open class SpringPackageCurationProvider( + override val descriptor: PluginDescriptor = SpringPackageCurationProviderFactory.descriptor +) : PackageCurationProvider { + val springBootProjectPaths = mutableMapOf>() + + fun getSpringBootProjectPath(subProjectName: String, projectVersion: String): String { + val paths = springBootProjectPaths.getOrPut(projectVersion) { + getSpringProjectPaths("spring-boot", projectVersion) + } + + return paths.getValue(subProjectName) + } + + override fun getCurationsFor(packages: Collection): Set { + val springPackages = packages.filter { + it.id.type == "Maven" && it.id.namespace.startsWith("org.springframework") + } + + if (springPackages.isEmpty()) return emptySet() + + val springCurations = mutableSetOf() + val metadataOnlyNames = setOf("boot", "cloud") + + springPackages.mapNotNullTo(springCurations) { pkg -> + var data = PackageCurationData() + + val isMetadataOnly = with(pkg.id) { + metadataOnlyNames.any { + namespace == "org.springframework.$it" + && (name.startsWith("spring-$it-starter") || name.startsWith("spring-$it-contract-spec")) + } + } + + if (isMetadataOnly) { + data = data.copy(isMetadataOnly = isMetadataOnly) + } + + if (pkg.vcsProcessed.url == "https://github.com/spring-projects/spring-boot.git") { + runCatching { + getSpringBootProjectPath(pkg.id.name, pkg.id.version) + }.onSuccess { path -> + data = data.copy(vcs = VcsInfoCurationData(path = path)) + } + } + + if (data != PackageCurationData()) { + PackageCuration(pkg.id, data) + } else { + null + } + } + + return springCurations + } +} diff --git a/plugins/package-curation-providers/spring/src/main/kotlin/Utils.kt b/plugins/package-curation-providers/spring/src/main/kotlin/Utils.kt new file mode 100644 index 0000000000000..88faf0c44977c --- /dev/null +++ b/plugins/package-curation-providers/spring/src/main/kotlin/Utils.kt @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2025 The ORT Project Authors (see ) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +package org.ossreviewtoolkit.plugins.packagecurationproviders.spring + +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive + +import org.ossreviewtoolkit.utils.common.withoutSuffix +import org.ossreviewtoolkit.utils.ort.downloadText +import org.ossreviewtoolkit.utils.ort.okHttpClient + +internal fun getGitHubTree(owner: String, repo: String, revision: String): List { + val json = okHttpClient + .downloadText("https://api.github.com/repos/$owner/$repo/git/trees/$revision?recursive=1") + .getOrThrow() + + return Json.parseToJsonElement(json).jsonObject.getValue("tree").jsonArray.map { + it.jsonObject.getValue("path").jsonPrimitive.content + } +} + +internal fun getSpringProjectPaths(projectName: String, projectVersion: String): Map { + val paths = getGitHubTree("spring-projects", projectName, "v$projectVersion") + + val projectPaths = paths.mapNotNull { path -> + path.withoutSuffix("/build.gradle")?.takeIf { it.startsWith(projectName) } + } + + val projectPathsByName = projectPaths.associateBy { it.substringAfterLast('/') } + + check(projectPathsByName.size == projectPaths.size) { + "Ambiguous mapping of project names to project paths." + } + + return projectPathsByName +} diff --git a/plugins/package-curation-providers/spring/src/test/kotlin/SpringPackageCurationProviderTest.kt b/plugins/package-curation-providers/spring/src/test/kotlin/SpringPackageCurationProviderTest.kt new file mode 100644 index 0000000000000..c8c3d2fa8dc70 --- /dev/null +++ b/plugins/package-curation-providers/spring/src/test/kotlin/SpringPackageCurationProviderTest.kt @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2025 The ORT Project Authors (see ) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +package org.ossreviewtoolkit.plugins.packagecurationproviders.spring + +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.collections.shouldContainExactly + +import org.ossreviewtoolkit.model.Identifier +import org.ossreviewtoolkit.model.Package +import org.ossreviewtoolkit.model.PackageCuration +import org.ossreviewtoolkit.model.PackageCurationData +import org.ossreviewtoolkit.model.VcsInfo +import org.ossreviewtoolkit.model.VcsInfoCurationData + +class SpringPackageCurationProviderTest : StringSpec({ + val provider = SpringPackageCurationProvider() + + "Recognize metadata only projects" { + val id = Identifier("Maven:org.springframework.boot:spring-boot-starter-parent:2.7.4") + val pkg = Package.EMPTY.copy(id = id) + + provider.getCurationsFor(setOf(pkg)) shouldContainExactly setOf( + PackageCuration( + id = id, + data = PackageCurationData(isMetadataOnly = true) + ) + ) + } + + "Get the correct paths for Spring Boot projects" { + val id = Identifier("Maven:org.springframework.boot:spring-boot-antlib:3.5.4") + val pkg = Package.EMPTY.copy( + id = id, + vcsProcessed = VcsInfo.EMPTY.copy(url = "https://github.com/spring-projects/spring-boot.git") + ) + + provider.getCurationsFor(setOf(pkg)) shouldContainExactly setOf( + PackageCuration( + id = id, + data = PackageCurationData( + vcs = VcsInfoCurationData(path = "spring-boot-project/spring-boot-tools/spring-boot-antlib") + ) + ) + ) + } +}) diff --git a/plugins/package-managers/gradle-inspector/src/main/kotlin/GradleDependencyHandler.kt b/plugins/package-managers/gradle-inspector/src/main/kotlin/GradleDependencyHandler.kt index fb4110cc83cec..bbf25494fa8df 100644 --- a/plugins/package-managers/gradle-inspector/src/main/kotlin/GradleDependencyHandler.kt +++ b/plugins/package-managers/gradle-inspector/src/main/kotlin/GradleDependencyHandler.kt @@ -80,14 +80,7 @@ internal class GradleDependencyHandler( return null } - val isSpringMetadataProject = with(id) { - listOf("boot", "cloud").any { - namespace == "org.springframework.$it" - && (name.startsWith("spring-$it-starter") || name.startsWith("spring-$it-contract-spec")) - } - } - - val hasNoArtifacts = dependency.pomFile == null || isSpringMetadataProject + val hasNoArtifacts = dependency.pomFile == null val binaryArtifact = when { hasNoArtifacts -> RemoteArtifact.EMPTY diff --git a/plugins/package-managers/maven/src/main/kotlin/utils/MavenSupport.kt b/plugins/package-managers/maven/src/main/kotlin/utils/MavenSupport.kt index b3337803888ab..cb1297dfde0a8 100644 --- a/plugins/package-managers/maven/src/main/kotlin/utils/MavenSupport.kt +++ b/plugins/package-managers/maven/src/main/kotlin/utils/MavenSupport.kt @@ -538,15 +538,6 @@ class MavenSupport(private val workspaceReader: WorkspaceReader) : Closeable { PackageManager.processProjectVcs(it, vcsFromPackage, *vcsFallbackUrls) } ?: PackageManager.processPackageVcs(vcsFromPackage, *vcsFallbackUrls) - val isSpringMetadataProject = with(mavenProject) { - listOf("boot", "cloud").any { - groupId == "org.springframework.$it" && ( - artifactId.startsWith("spring-$it-starter") || - artifactId.startsWith("spring-$it-contract-spec") - ) - } - } - return Package( id = Identifier( type = "Maven", @@ -563,8 +554,7 @@ class MavenSupport(private val workspaceReader: WorkspaceReader) : Closeable { sourceArtifact = sourceRemoteArtifact, vcs = vcsFromPackage, vcsProcessed = vcsProcessed, - isMetadataOnly = (mavenProject.packaging == "pom" && binaryRemoteArtifact.url.endsWith(".pom")) - || isSpringMetadataProject, + isMetadataOnly = mavenProject.packaging == "pom" && binaryRemoteArtifact.url.endsWith(".pom"), isModified = isBinaryArtifactModified || isSourceArtifactModified ) }