diff --git a/build-logic/src/main/kotlin/asf/AsfProject.kt b/build-logic/src/main/kotlin/asf/AsfProject.kt new file mode 100644 index 0000000000..c997751ed0 --- /dev/null +++ b/build-logic/src/main/kotlin/asf/AsfProject.kt @@ -0,0 +1,187 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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. + */ + +package asf + +import groovy.json.JsonException +import groovy.json.JsonSlurper +import java.io.FileNotFoundException +import java.net.URI +import org.gradle.api.Project +import org.gradle.kotlin.dsl.extra + +class AsfProject( + val apacheId: String, + val name: String, + val description: String, + val website: String, + val repository: String, + val licenseUrl: String, + val bugDatabase: String, + val inceptionYear: Int, +) { + companion object { + + fun memoized(project: Project, asfName: String): AsfProject { + val rootProject = project.rootProject + return if (rootProject.extra.has("asfProject")) { + unsafeCast(rootProject.extra["asfProject"]) as AsfProject + } else { + val asfProject = fetchProjectInformation(asfName) + rootProject.extra["asfProject"] = asfProject + return asfProject + } + } + + internal fun unsafeCast(o: Any?): T { + @Suppress("UNCHECKED_CAST") + return o as T + } + + internal fun parseJson(urlStr: String): T { + val url = URI(urlStr).toURL() + + val headers = mutableMapOf() + if (url.host == "api.github.com" && url.protocol == "https") { + val githubToken = System.getenv("GITHUB_TOKEN") + if (githubToken != null) { + // leverage the GH token to benefit from higher rate limits + headers["Authorization"] = "Bearer $githubToken" + // recommended for GH API requests + headers["X-GitHub-Api-Version"] = "2022-11-28" + headers["Accept"] = "application/vnd.github+json" + } + } + + // See org.codehaus.groovy.runtime.ResourceGroovyMethods.newReader(URL, Map) + val params = mapOf("requestProperties" to headers) + + val slurper = JsonSlurper() + var attempt = 0 + while (true) { + try { + return unsafeCast(slurper.parse(params, url)) as T + } catch (e: JsonException) { + if (e.cause is FileNotFoundException) { + throw e + } + if (attempt == 5) { + throw e + } + Thread.sleep(1000L) + } + attempt++ + } + } + + /** Retrieves the project name, for example `Polaris` using the lower-case project ID. */ + internal fun fetchAsfProjectName(apacheId: String): String { + val project = projectMap(apacheId) + val isPodlingCurrent = project.containsKey("podling") && project["podling"] == "current" + if (isPodlingCurrent) { + val podling = podlingMap(apacheId) + return podling["name"] as String + } else { + // top-level-project + val committee = projectCommitteeMap(apacheId) + return committee["display_name"] as String + } + } + + internal fun projectCommitteeMap(apacheId: String): Map { + val committeesAll: Map> = + parseJson("https://whimsy.apache.org/public/committee-info.json") + val committees = unsafeCast>>(committeesAll["committees"]) + return unsafeCast(committees[apacheId]) + } + + internal fun projectMap(apacheId: String): Map { + val projectsAll: Map> = + parseJson("https://whimsy.apache.org/public/public_ldap_projects.json") + val projects = unsafeCast>>(projectsAll["projects"]) + val project = + projects[apacheId] + ?: throw IllegalArgumentException( + "No project '$apacheId' found in https://whimsy.apache.org/public/public_ldap_projects.json" + ) + return project + } + + internal fun podlingMap(apacheId: String): Map { + val podlingsAll: Map> = + parseJson("https://whimsy.apache.org/public/public_podlings.json") + val podlings = unsafeCast>>(podlingsAll["podling"]) + val podling = + podlings[apacheId] + ?: throw IllegalArgumentException( + "No podling '$apacheId' found in https://whimsy.apache.org/public/public_podlings.json" + ) + return podling + } + + internal fun fetchProjectInformation(apacheId: String): AsfProject { + val project = projectMap(apacheId) + val isPodlingCurrent = project.containsKey("podling") && project["podling"] == "current" + + val inceptionYear = + (project["createTimestamp"] as String).subSequence(0, 4).toString().toInt() + + val projectName: String + val description: String + val website: String + val repository: String + val licenseUrl: String + val bugDatabase: String + if (isPodlingCurrent) { + val podling = podlingMap(apacheId) + projectName = podling["name"] as String + description = podling["description"] as String + val podlingStatus = unsafeCast(podling["podlingStatus"]) as Map + website = podlingStatus["website"] as String + // No repository for podlings?? + repository = "https://github.com/apache/$apacheId.git" + bugDatabase = "https://github.com/apache/$apacheId/issues" + licenseUrl = "https://www.apache.org/licenses/LICENSE-2.0.txt" + } else { + // top-level-project + val tlpPrj: Map = + parseJson("https://projects.apache.org/json/projects/$apacheId.json") + website = tlpPrj["homepage"] as String + repository = (unsafeCast(tlpPrj["repository"]) as List)[0] + bugDatabase = tlpPrj["bug-database"] as String + licenseUrl = tlpPrj["license"] as String + + val committee = projectCommitteeMap(apacheId) + projectName = committee["display_name"] as String + description = committee["description"] as String + } + + return AsfProject( + apacheId, + projectName, + description, + website, + repository, + licenseUrl, + bugDatabase, + inceptionYear, + ) + } + } +} diff --git a/build-logic/src/main/kotlin/publishing/configurePom.kt b/build-logic/src/main/kotlin/publishing/configurePom.kt index c140dd2cd7..36e3aa58f6 100644 --- a/build-logic/src/main/kotlin/publishing/configurePom.kt +++ b/build-logic/src/main/kotlin/publishing/configurePom.kt @@ -19,6 +19,7 @@ package publishing +import asf.AsfProject import groovy.util.Node import org.gradle.api.Project import org.gradle.api.Task @@ -74,8 +75,8 @@ internal fun configurePom(project: Project, mavenPublication: MavenPublication, task.doFirst { mavenPom.run { - val asfName = e.asfProjectId.get() - val projectInfo = fetchProjectInformation(asfName) + val asfProject = AsfProject.memoized(project, e.asfProjectId.get()) + val asfProjectId = asfProject.apacheId organization { name.set("The Apache Software Foundation") @@ -84,27 +85,27 @@ internal fun configurePom(project: Project, mavenPublication: MavenPublication, licenses { license { name.set("Apache-2.0") // SPDX identifier - url.set(projectInfo.licenseUrl) + url.set(asfProject.licenseUrl) } } mailingLists { e.mailingLists.get().forEach { ml -> mailingList { name.set("${ml.capitalized()} Mailing List") - subscribe.set("$ml-subscribe@$asfName.apache.org") - unsubscribe.set("$ml-unsubscribe@$asfName.apache.org") - post.set("$ml@$asfName.apache.org") - archive.set("https://lists.apache.org/list.html?$ml@$asfName.apache.org") + subscribe.set("$ml-subscribe@$asfProjectId.apache.org") + unsubscribe.set("$ml-unsubscribe@$asfProjectId.apache.org") + post.set("$ml@$asfProjectId.apache.org") + archive.set("https://lists.apache.org/list.html?$ml@$asfProjectId.apache.org") } } } - val githubRepoName: Provider = e.githubRepositoryName.orElse(asfName) + val githubRepoName: Provider = e.githubRepositoryName.orElse(asfProjectId) val codeRepo: Provider = e.overrideScm.orElse( githubRepoName .map { r -> "https://github.com/apache/$r" } - .orElse(projectInfo.repository) + .orElse(asfProject.repository) ) scm { @@ -115,22 +116,22 @@ internal fun configurePom(project: Project, mavenPublication: MavenPublication, val version = project.version.toString() if (!version.endsWith("-SNAPSHOT")) { val tagPrefix: String = - e.overrideTagPrefix.orElse("apache-${projectInfo.apacheId}").get() + e.overrideTagPrefix.orElse("apache-${asfProject.apacheId}").get() tag.set("$tagPrefix-$version") } } issueManagement { val issuesUrl: Provider = - codeRepo.map { r -> "$r/issues" }.orElse(projectInfo.bugDatabase) + codeRepo.map { r -> "$r/issues" }.orElse(asfProject.bugDatabase) url.set(e.overrideIssueManagement.orElse(issuesUrl)) } - name.set(e.overrideName.orElse("Apache ${projectInfo.name}")) - description.set(e.overrideDescription.orElse(projectInfo.description)) - url.set(e.overrideProjectUrl.orElse(projectInfo.website)) - inceptionYear.set(projectInfo.inceptionYear.toString()) + name.set(e.overrideName.orElse("Apache ${asfProject.name}")) + description.set(e.overrideDescription.orElse(asfProject.description)) + url.set(e.overrideProjectUrl.orElse(asfProject.website)) + inceptionYear.set(asfProject.inceptionYear.toString()) - developers { developer { url.set("https://$asfName.apache.org/community/") } } + developers { developer { url.set("https://$asfProjectId.apache.org/community/") } } } } } diff --git a/build-logic/src/main/kotlin/publishing/rootProject.kt b/build-logic/src/main/kotlin/publishing/rootProject.kt index 001d9ccee7..819a619281 100644 --- a/build-logic/src/main/kotlin/publishing/rootProject.kt +++ b/build-logic/src/main/kotlin/publishing/rootProject.kt @@ -19,6 +19,7 @@ package publishing +import asf.AsfProject import io.github.gradlenexus.publishplugin.NexusPublishExtension import io.github.gradlenexus.publishplugin.NexusPublishPlugin import io.github.gradlenexus.publishplugin.internal.StagingRepositoryDescriptorRegistryBuildService @@ -134,8 +135,9 @@ internal fun configureOnRootProject(project: Project) = "NO STAGING REPOSITORY (no build service) !!" } + val asfProject = AsfProject.memoized(project, asfName) val asfProjectName = - e.overrideName.orElse(project.provider { "Apache ${fetchAsfProjectName(asfName)}" }).get() + e.overrideName.orElse(project.provider { "Apache ${asfProject.name}" }).get() val versionNoRc = version.toString().replace("-rc-?[0-9]+".toRegex(), "") diff --git a/build-logic/src/main/kotlin/publishing/util.kt b/build-logic/src/main/kotlin/publishing/util.kt index fd1366731d..fb40855cef 100644 --- a/build-logic/src/main/kotlin/publishing/util.kt +++ b/build-logic/src/main/kotlin/publishing/util.kt @@ -19,12 +19,8 @@ package publishing -import groovy.json.JsonException -import groovy.json.JsonSlurper import groovy.util.Node import groovy.util.NodeList -import java.io.FileNotFoundException -import java.net.URI import org.gradle.api.artifacts.Configuration import org.gradle.api.artifacts.component.ModuleComponentSelector import org.gradle.api.artifacts.result.DependencyResult @@ -55,148 +51,3 @@ internal fun xmlNode(node: Node?, child: String): Node? { } return null } - -internal fun unsafeCast(o: Any?): T { - @Suppress("UNCHECKED_CAST") - return o as T -} - -internal fun parseJson(urlStr: String): T { - val url = URI(urlStr).toURL() - - val headers = mutableMapOf() - if (url.host == "api.github.com" && url.protocol == "https") { - val githubToken = System.getenv("GITHUB_TOKEN") - if (githubToken != null) { - // leverage the GH token to benefit from higher rate limits - headers["Authorization"] = "Bearer $githubToken" - // recommended for GH API requests - headers["X-GitHub-Api-Version"] = "2022-11-28" - headers["Accept"] = "application/vnd.github+json" - } - } - - // See org.codehaus.groovy.runtime.ResourceGroovyMethods.newReader(URL, Map) - val params = mapOf("requestProperties" to headers) - - val slurper = JsonSlurper() - var attempt = 0 - while (true) { - try { - return unsafeCast(slurper.parse(params, url)) as T - } catch (e: JsonException) { - if (e.cause is FileNotFoundException) { - throw e - } - if (attempt == 5) { - throw e - } - Thread.sleep(1000L) - } - attempt++ - } -} - -/** Retrieves the project name, for example `Polaris` using the lower-case project ID. */ -internal fun fetchAsfProjectName(apacheId: String): String { - val project = projectMap(apacheId) - val isPodlingCurrent = project.containsKey("podling") && project["podling"] == "current" - if (isPodlingCurrent) { - val podling = podlingMap(apacheId) - return podling["name"] as String - } else { - // top-level-project - val committee = projectCommitteeMap(apacheId) - return committee["display_name"] as String - } -} - -internal fun projectCommitteeMap(apacheId: String): Map { - val committeesAll: Map> = - parseJson("https://whimsy.apache.org/public/committee-info.json") - val committees = unsafeCast>>(committeesAll["committees"]) - return unsafeCast(committees[apacheId]) -} - -internal fun projectMap(apacheId: String): Map { - val projectsAll: Map> = - parseJson("https://whimsy.apache.org/public/public_ldap_projects.json") - val projects = unsafeCast>>(projectsAll["projects"]) - val project = - projects[apacheId] - ?: throw IllegalArgumentException( - "No project '$apacheId' found in https://whimsy.apache.org/public/public_ldap_projects.json" - ) - return project -} - -internal fun podlingMap(apacheId: String): Map { - val podlingsAll: Map> = - parseJson("https://whimsy.apache.org/public/public_podlings.json") - val podlings = unsafeCast>>(podlingsAll["podling"]) - val podling = - podlings[apacheId] - ?: throw IllegalArgumentException( - "No podling '$apacheId' found in https://whimsy.apache.org/public/public_podlings.json" - ) - return podling -} - -internal fun fetchProjectInformation(apacheId: String): ProjectInformation { - val project = projectMap(apacheId) - val isPodlingCurrent = project.containsKey("podling") && project["podling"] == "current" - - val inceptionYear = (project["createTimestamp"] as String).subSequence(0, 4).toString().toInt() - - val projectName: String - val description: String - val website: String - val repository: String - val licenseUrl: String - val bugDatabase: String - if (isPodlingCurrent) { - val podling = podlingMap(apacheId) - projectName = podling["name"] as String - description = podling["description"] as String - val podlingStatus = unsafeCast(podling["podlingStatus"]) as Map - website = podlingStatus["website"] as String - // No repository for podlings?? - repository = "https://github.com/apache/$apacheId.git" - bugDatabase = "https://github.com/apache/$apacheId/issues" - licenseUrl = "https://www.apache.org/licenses/LICENSE-2.0.txt" - } else { - // top-level-project - val tlpPrj: Map = - parseJson("https://projects.apache.org/json/projects/$apacheId.json") - website = tlpPrj["homepage"] as String - repository = (unsafeCast(tlpPrj["repository"]) as List)[0] - bugDatabase = tlpPrj["bug-database"] as String - licenseUrl = tlpPrj["license"] as String - - val committee = projectCommitteeMap(apacheId) - projectName = committee["display_name"] as String - description = committee["description"] as String - } - - return ProjectInformation( - apacheId, - projectName, - description, - website, - repository, - licenseUrl, - bugDatabase, - inceptionYear, - ) -} - -internal class ProjectInformation( - val apacheId: String, - val name: String, - val description: String, - val website: String, - val repository: String, - val licenseUrl: String, - val bugDatabase: String, - val inceptionYear: Int, -)