[PKGS] Replace deprecated PackageSearch API V2 client with V3

GitOrigin-RevId: 18302e08783e4ea0cbfe5dcad17ce095a754ceb0
This commit is contained in:
fscarponi
2024-06-27 12:12:05 +02:00
committed by intellij-monorepo-bot
parent ceb99fa5c5
commit 3afb5039cf
17 changed files with 178 additions and 752 deletions

View File

@@ -0,0 +1,59 @@
<component name="libraryTable">
<library name="package-search-api-client" type="repository">
<properties maven-id="org.jetbrains.packagesearch:packagesearch-api-client-jvm:3.1.1">
<verification>
<artifact url="file://$MAVEN_REPOSITORY$/org/jetbrains/packagesearch/packagesearch-api-client-jvm/3.1.1/packagesearch-api-client-jvm-3.1.1.jar">
<sha256sum>15cf65b66dc121308517db68e82d8930d7e227257bdb862ca07f1328515eea6b</sha256sum>
</artifact>
<artifact url="file://$MAVEN_REPOSITORY$/org/jetbrains/packagesearch/packagesearch-http-models-jvm/3.1.1/packagesearch-http-models-jvm-3.1.1.jar">
<sha256sum>d8eeccae53547378ea2d1aaab6efa94d65c4693e3bcdefed13c44b8ce8a52df9</sha256sum>
</artifact>
<artifact url="file://$MAVEN_REPOSITORY$/org/jetbrains/packagesearch/packagesearch-api-models-jvm/3.1.1/packagesearch-api-models-jvm-3.1.1.jar">
<sha256sum>eac34c241019a23d3b0a07b4e04888b110aa4c06a826f80de19d2457f6be4b81</sha256sum>
</artifact>
<artifact url="file://$MAVEN_REPOSITORY$/org/jetbrains/packagesearch/packagesearch-version-utils-jvm/3.1.1/packagesearch-version-utils-jvm-3.1.1.jar">
<sha256sum>ec7192861556914e6edb5ce0b901848431b03a327a3ca4045c332ab5b3b24ff8</sha256sum>
</artifact>
<artifact url="file://$MAVEN_REPOSITORY$/com/soywiz/korlibs/krypto/krypto-jvm/4.0.10/krypto-jvm-4.0.10.jar">
<sha256sum>0fe8dcdf54b13b5ec56fdb5f63c057364264bb2f51b7f7bc3c271d5b1ba68dcb</sha256sum>
</artifact>
</verification>
<exclude>
<dependency maven-id="io.ktor:ktor-client-cio-jvm" />
<dependency maven-id="ch.qos.logback:logback-classic" />
<dependency maven-id="org.jetbrains.kotlin:kotlin-stdlib-jdk8" />
<dependency maven-id="org.jetbrains.kotlinx:kotlinx-datetime-jvm" />
<dependency maven-id="org.jetbrains.kotlinx:kotlinx-serialization-json-jvm" />
<dependency maven-id="org.jetbrains.kotlin:kotlin-stdlib-common" />
<dependency maven-id="io.ktor:ktor-http-jvm" />
<dependency maven-id="io.ktor:ktor-client-content-negotiation-jvm" />
<dependency maven-id="io.ktor:ktor-client-encoding-jvm" />
<dependency maven-id="io.ktor:ktor-serialization-kotlinx-json-jvm" />
<dependency maven-id="org.jetbrains.kotlin:kotlin-stdlib-jdk7" />
<dependency maven-id="org.jetbrains.kotlinx:kotlinx-coroutines-jdk8" />
<dependency maven-id="org.slf4j:slf4j-api" />
<dependency maven-id="org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm" />
<dependency maven-id="io.ktor:ktor-serialization-kotlinx-jvm" />
<dependency maven-id="org.jetbrains.kotlinx:kotlinx-serialization-protobuf-jvm" />
<dependency maven-id="org.jetbrains:annotations" />
<dependency maven-id="io.ktor:ktor-serialization-kotlinx-protobuf-jvm" />
<dependency maven-id="org.jetbrains.kotlin:kotlin-stdlib" />
</exclude>
</properties>
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/packagesearch/packagesearch-api-client-jvm/3.1.1/packagesearch-api-client-jvm-3.1.1.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/packagesearch/packagesearch-http-models-jvm/3.1.1/packagesearch-http-models-jvm-3.1.1.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/packagesearch/packagesearch-api-models-jvm/3.1.1/packagesearch-api-models-jvm-3.1.1.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/packagesearch/packagesearch-version-utils-jvm/3.1.1/packagesearch-version-utils-jvm-3.1.1.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/com/soywiz/korlibs/krypto/krypto-jvm/4.0.10/krypto-jvm-4.0.10.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/packagesearch/packagesearch-api-client-jvm/3.1.1/packagesearch-api-client-jvm-3.1.1-sources.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/packagesearch/packagesearch-http-models-jvm/3.1.1/packagesearch-http-models-jvm-3.1.1-sources.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/packagesearch/packagesearch-api-models-jvm/3.1.1/packagesearch-api-models-jvm-3.1.1-sources.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/packagesearch/packagesearch-version-utils-jvm/3.1.1/packagesearch-version-utils-jvm-3.1.1-sources.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/com/soywiz/korlibs/krypto/krypto-jvm/4.0.10/krypto-jvm-4.0.10-sources.jar!/" />
</SOURCES>
</library>
</component>

View File

@@ -1,18 +0,0 @@
<component name="libraryTable">
<library name="package-search-api-models" type="repository">
<properties include-transitive-deps="false" maven-id="org.jetbrains.packagesearch:pkgs-api-models:2.5.1">
<verification>
<artifact url="file://$MAVEN_REPOSITORY$/org/jetbrains/packagesearch/pkgs-api-models/2.5.1/pkgs-api-models-2.5.1.jar">
<sha256sum>5cbd054ec28f66e9ababfc46ed3129cdcdbb0174dfc3d9c50ab8784b1b2dffcb</sha256sum>
</artifact>
</verification>
</properties>
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/packagesearch/pkgs-api-models/2.5.1/pkgs-api-models-2.5.1.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/packagesearch/pkgs-api-models/2.5.1/pkgs-api-models-2.5.1-sources.jar!/" />
</SOURCES>
</library>
</component>

View File

@@ -871,9 +871,9 @@ object CommunityLibraryLicenses {
LibraryLicense(name = "OverlayScrollbars", attachedTo = "intellij.idea.community.main",
url = "https://kingsora.github.io/OverlayScrollbars", version = "2.1.1")
.mit("https://github.com/KingSora/OverlayScrollbars/blob/master/LICENSE"),
LibraryLicense(name = "Package Search API Models", libraryName = "package-search-api-models",
url = "https://github.com/JetBrains/package-search-api-models").apache(),
LibraryLicense(name = "Package Search API-Client", libraryName = "package-search-api-client", url = "https://github.com/JetBrains/package-search-api-models")
.apache("https://github.com/JetBrains/package-search-api-models/blob/master/LICENSE")
.suppliedByOrganizations("JetBrains Team"),
LibraryLicense(name = "pip", attachedTo = "intellij.python", version = "20.3.4",
url = "https://pip.pypa.io/")
.mit("https://github.com/pypa/pip/blob/main/LICENSE.txt"),

View File

@@ -40,7 +40,6 @@
<orderEntry type="module" module-name="intellij.platform.statistics" />
<orderEntry type="library" name="gson" level="project" />
<orderEntry type="module" module-name="intellij.platform.ide.util.io" />
<orderEntry type="library" name="package-search-api-models" level="project" />
<orderEntry type="library" name="kotlin-stdlib" level="project" />
<orderEntry type="library" name="kotlinx-coroutines-core" level="project" />
<orderEntry type="library" exported="" name="kotlinx-serialization-core" level="project" />
@@ -50,5 +49,6 @@
<orderEntry type="library" scope="TEST" name="ktor-client-mock" level="project" />
<orderEntry type="library" scope="TEST" name="kotlinx-coroutines-test" level="project" />
<orderEntry type="module" module-name="intellij.libraries.ktor.client" />
<orderEntry type="library" exported="" name="package-search-api-client" level="project" />
</component>
</module>

View File

@@ -1,34 +0,0 @@
/*
* Copyright 2000-2022 JetBrains s.r.o. and contributors.
*
* 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.
*/
package org.jetbrains.idea.packagesearch
import com.intellij.openapi.components.Service
import com.intellij.openapi.util.registry.Registry
import kotlin.time.Duration.Companion.milliseconds
internal object ServerURLs {
const val base = "package-search.services.jetbrains.com"
}
@Service(Service.Level.APP)
class DefaultPackageServiceConfig : PackageSearchServiceConfig {
override val host: String = ServerURLs.base
override val timeout = Registry.intValue("packagesearch.timeout").milliseconds
override val useCache = true
}

View File

@@ -1,22 +0,0 @@
/*
* Copyright 2000-2022 JetBrains s.r.o. and contributors.
*
* 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.
*/
package org.jetbrains.idea.packagesearch
enum class HashingAlgorithm {
MD5,
SHA1
}

View File

@@ -19,18 +19,15 @@ package org.jetbrains.idea.packagesearch
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.registry.RegistryManager
import org.jetbrains.idea.packagesearch.api.LifecycleScope
import org.jetbrains.idea.packagesearch.api.PackageSearchProvider
import org.jetbrains.idea.packagesearch.api.PackageSearchApiClientService
import org.jetbrains.idea.reposearch.DependencySearchProvider
import org.jetbrains.idea.reposearch.DependencySearchProvidersFactory
internal class PackageSearchProviderFactory : DependencySearchProvidersFactory {
override fun getProviders(project: Project): Collection<DependencySearchProvider> {
return if (!RegistryManager.getInstance().`is`("maven.packagesearch.enabled")) {
emptyList()
}
else {
setOf<DependencySearchProvider>(PackageSearchProvider(project))
return when {
!RegistryManager.getInstance().`is`("maven.packagesearch.enabled") -> emptyList()
else -> setOf(service<PackageSearchApiClientService>())
}
}
}

View File

@@ -1,57 +0,0 @@
/*
* Copyright 2000-2022 JetBrains s.r.o. and contributors.
*
* 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.
*/
package org.jetbrains.idea.packagesearch
import com.intellij.openapi.application.ApplicationInfo
import com.intellij.openapi.application.ApplicationNamesInfo
import io.ktor.client.plugins.logging.LogLevel
import io.ktor.client.plugins.logging.Logger
import io.ktor.http.HttpProtocolVersion
import io.ktor.http.URLProtocol
import org.jetbrains.idea.packagesearch.api.buildHeaders
import org.jetbrains.idea.packagesearch.api.simple
import org.jetbrains.idea.reposearch.PluginEnvironment
import org.jetbrains.idea.reposearch.logTrace
import kotlin.time.Duration
interface PackageSearchServiceConfig {
val host: String
val userAgent: String
get() = ApplicationNamesInfo.getInstance().productName + "/" + ApplicationInfo.getInstance().fullVersion
val headers
get() = buildHeaders(2) {
append("JB-Plugin-Version", PluginEnvironment.pluginVersion)
append("JB-IDE-Version", PluginEnvironment.ideVersion)
}
val timeout: Duration
val useCache: Boolean
get() = false
val protocol
get() = URLProtocol.HTTPS
val logLevel
get() = LogLevel.HEADERS
val logger
get() = Logger.simple { logTrace(it) }
}

View File

@@ -1,13 +0,0 @@
package org.jetbrains.idea.packagesearch
enum class SortMetric(private val representationName: String, val parameterName: String) {
NONE("None", ""),
GITHUB_STARS("GitHub stars", "github_stars"),
STACKOVERFLOW_HEALTH("StackOverflow health", "stackoverflow_health"),
DEPENDENCY_RATING("Dependency rating", "dependency_rating"),
OSS_HEALTH("OSS health", "oss_health");
override fun toString(): String {
return representationName
}
}

View File

@@ -1,86 +0,0 @@
/*
* Copyright 2000-2022 JetBrains s.r.o. and contributors.
*
* 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.
*/
package org.jetbrains.idea.packagesearch.api
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.Service.Level.PROJECT
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import io.ktor.client.engine.HttpClientEngine
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.future.future
import org.jetbrains.idea.packagesearch.DefaultPackageServiceConfig
import org.jetbrains.idea.packagesearch.HashingAlgorithm
import org.jetbrains.idea.packagesearch.PackageSearchServiceConfig
import org.jetbrains.idea.packagesearch.SortMetric
import org.jetbrains.packagesearch.api.v2.ApiPackagesResponse
import org.jetbrains.packagesearch.api.v2.ApiStandardPackage
import java.util.concurrent.CompletableFuture
@Service(PROJECT)
internal class LifecycleScope(val cs: CoroutineScope)
fun AsyncPackageSearchApiClient(
project: Project,
config: PackageSearchServiceConfig = service<DefaultPackageServiceConfig>(),
engine: HttpClientEngine? = null
) = AsyncPackageSearchApiClient(project.service<LifecycleScope>().cs, config, engine)
class AsyncPackageSearchApiClient(
private val scope: CoroutineScope,
config: PackageSearchServiceConfig = service<DefaultPackageServiceConfig>(),
engine: HttpClientEngine? = null
) {
private val myClient = PackageSearchApiClient(config, engine)
fun packagesByQuery(
searchQuery: String,
onlyStable: Boolean,
onlyMpp: Boolean,
sortMetric: SortMetric,
repositoryIds: List<String>
): CompletableFuture<ApiPackagesResponse<ApiStandardPackage, ApiStandardPackage.ApiStandardVersion>> =
scope.future { myClient.packagesByQuery(searchQuery, onlyStable, onlyMpp, sortMetric, repositoryIds) }
fun suggestPackages(
groupId: String?,
artifactId: String?,
onlyMpp: Boolean,
repositoryIds: List<String>
): CompletableFuture<ApiPackagesResponse<ApiStandardPackage, ApiStandardPackage.ApiStandardVersion>> =
scope.future { myClient.suggestPackages(groupId, artifactId, onlyMpp, repositoryIds) }
fun packagesByRange(range: List<String>) =
scope.future { myClient.packagesByRange(range) }
fun packageByHash(hash: String, hashingAlgorithm: HashingAlgorithm) =
scope.future { myClient.packageByHash(hash, hashingAlgorithm) }
fun packageById(id: String) =
scope.future { myClient.packageById(id) }
fun readmeByPackageId(id: String) =
scope.future { myClient.readmeByPackageId(id) }
fun statistics() =
scope.future { myClient.statistics() }
fun repositories() =
scope.future { myClient.repositories() }
}

View File

@@ -1,66 +0,0 @@
package org.jetbrains.idea.packagesearch.api
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.plugins.HttpTimeout
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.logging.Logger
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.get
import io.ktor.client.request.headers
import io.ktor.http.*
import io.ktor.serialization.kotlinx.KotlinxSerializationConverter
import io.ktor.util.StringValuesBuilder
import kotlinx.serialization.json.Json
import org.jetbrains.packagesearch.api.v2.ApiPackagesResponse
import org.jetbrains.packagesearch.api.v2.ApiStandardPackage
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
internal object PackageSearchApiContentTypes {
val StandardV2
get() = ContentType("application", "vnd.jetbrains.packagesearch.standard.v2+json")
val MinimalV2
get() = ContentType("application", "vnd.jetbrains.packagesearch.minimal.v2+json")
}
internal var HttpTimeout.HttpTimeoutCapabilityConfiguration.requestTimeout: Duration?
get() = connectTimeoutMillis?.milliseconds
set(value) {
connectTimeoutMillis = value?.inWholeMilliseconds
}
@Suppress("UnusedReceiverParameter")
internal val ContentType.Application.PackageSearch
get() = PackageSearchApiContentTypes
internal val emptyStandardV2PackagesWithRepos = ApiPackagesResponse<ApiStandardPackage, ApiStandardPackage.ApiStandardVersion>(
packages = emptyList(),
repositories = emptyList()
)
internal fun ContentNegotiation.Config.packageSearch(json: Json = Json) {
register(ContentType.Application.PackageSearch.StandardV2, KotlinxSerializationConverter(json))
}
internal fun StringValuesBuilder.append(name: String, value: Iterable<Any>) = append(name, value.joinToString(","))
internal fun StringValuesBuilder.append(name: String, value: Any?) = append(name, value.toString())
internal fun URLBuilder.parameters(config: ParametersBuilder.() -> Unit) {
parameters.apply(config)
}
internal fun HttpMessageBuilder.headers(headers: Headers) {
headers {
headers.forEach { s, strings -> appendAll(s, strings) }
}
}
internal fun Logger.Companion.simple(log: (String) -> Unit) = object : Logger {
override fun log(message: String) = log(message)
}
internal fun buildHeaders(size: Int, build: HeadersBuilder.() -> Unit) = HeadersBuilder(size).apply(build).build()
internal suspend inline fun <reified T : Any> HttpClient.getBody(config: HttpRequestBuilder.() -> Unit): T =
get(config).body()

View File

@@ -1,232 +0,0 @@
/*
* Copyright 2000-2022 JetBrains s.r.o. and contributors.
*
* 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.
*/
package org.jetbrains.idea.packagesearch.api
import com.intellij.openapi.components.service
import io.ktor.client.HttpClient
import io.ktor.client.HttpClientConfig
import io.ktor.client.engine.HttpClientEngine
import io.ktor.client.engine.java.Java
import io.ktor.client.plugins.*
import io.ktor.client.plugins.cache.HttpCache
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.logging.Logging
import io.ktor.client.request.accept
import io.ktor.client.statement.bodyAsText
import io.ktor.client.statement.request
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import org.jetbrains.idea.packagesearch.DefaultPackageServiceConfig
import org.jetbrains.idea.packagesearch.HashingAlgorithm
import org.jetbrains.idea.packagesearch.PackageSearchServiceConfig
import org.jetbrains.idea.packagesearch.SortMetric
import org.jetbrains.idea.reposearch.DependencySearchBundle
import org.jetbrains.packagesearch.api.statistics.ApiStatisticsResponse
import org.jetbrains.packagesearch.api.v2.ApiPackageResponse
import org.jetbrains.packagesearch.api.v2.ApiPackagesResponse
import org.jetbrains.packagesearch.api.v2.ApiRepositoriesResponse
import org.jetbrains.packagesearch.api.v2.ApiStandardPackage
import java.io.Closeable
class PackageSearchApiClient internal constructor(
private val config: PackageSearchServiceConfig = service<DefaultPackageServiceConfig>(),
engine: HttpClientEngine?,
) : Closeable {
// do not expose HttpClientEngine
constructor() : this(engine = null)
data class ApiException(val serverMessage: String, val endpoint: String, val statusCode: HttpStatusCode) : Throwable() {
override val message: String
get() = "Error response for endpoint $endpoint: $statusCode | $serverMessage"
}
@Serializable
private data class Error(val error: Message) {
@Serializable
data class Message(val message: String)
}
private val clientConfig: HttpClientConfig<*>.() -> Unit
get() = {
val json = Json {
ignoreUnknownKeys = true
encodeDefaults = false
}
install(ContentNegotiation) {
packageSearch(json)
json(json)
}
install(Logging) {
logger = config.logger
level = config.logLevel
}
install(UserAgent) {
agent = config.userAgent
}
install(DefaultRequest) {
url {
protocol = config.protocol
host = config.host
path("api/")
}
headers(config.headers)
}
install(HttpTimeout) {
requestTimeout = config.timeout
}
install(HttpRequestRetry) {
retryOnServerErrors(5)
constantDelay()
}
install(HttpCallValidator) {
validateResponse { response ->
if (!response.status.isSuccess())
response.bodyAsText().runCatching { json.decodeFromString<Error>(this) }
.onSuccess { throw ApiException(it.error.message, response.request.url.fullPath, response.status) }
}
}
if (config.useCache) install(HttpCache)
}
private val httpClient = if (engine != null) HttpClient(engine, clientConfig) else HttpClient(Java, clientConfig)
private val maxRequestResultsCount = 25
private val maxMavenCoordinatesParts = 3
suspend fun packagesByQuery(
searchQuery: String,
onlyStable: Boolean = false,
onlyMpp: Boolean = false,
sortMetric: SortMetric = SortMetric.NONE,
repositoryIds: List<String> = emptyList()
): ApiPackagesResponse<ApiStandardPackage, ApiStandardPackage.ApiStandardVersion> {
if (searchQuery.isEmpty()) {
return emptyStandardV2PackagesWithRepos
}
return httpClient.getBody {
url {
appendEncodedPathSegments("package")
parameters {
append("query", searchQuery)
append("onlyStable", onlyStable)
append("onlyMpp", onlyMpp)
if (sortMetric != SortMetric.NONE) {
append("sort_by", sortMetric.parameterName)
}
if (repositoryIds.isNotEmpty()) {
append("repositoryIds", repositoryIds)
}
}
}
}
}
suspend fun suggestPackages(
groupId: String?,
artifactId: String?,
onlyMpp: Boolean = false,
repositoryIds: List<String> = emptyList()
): ApiPackagesResponse<ApiStandardPackage, ApiStandardPackage.ApiStandardVersion> {
if (groupId == null && artifactId == null) {
return emptyStandardV2PackagesWithRepos
}
return httpClient.getBody {
url {
appendEncodedPathSegments("package")
parameters {
append("groupid", groupId ?: "")
append("artifactid", artifactId ?: "")
append("onlyMpp", onlyMpp)
if (repositoryIds.isNotEmpty()) {
append("repositoryIds", repositoryIds)
}
}
}
}
}
suspend fun packagesByRange(range: List<String>): ApiPackagesResponse<ApiStandardPackage, ApiStandardPackage.ApiStandardVersion> {
if (range.isEmpty()) {
return emptyStandardV2PackagesWithRepos
}
require(range.size <= maxRequestResultsCount) {
DependencySearchBundle.message("reposearch.search.client.error.too.many.requests.for.range")
}
require(range.none { it.split(":").size >= maxMavenCoordinatesParts }) {
DependencySearchBundle.message("reposearch.search.client.error.no.versions.for.range")
}
return httpClient.getBody {
url {
appendEncodedPathSegments("package")
parameters {
append("range", range)
}
}
}
}
suspend fun packageByHash(
hash: String,
hashingAlgorithm: HashingAlgorithm
): ApiPackageResponse<ApiStandardPackage, ApiStandardPackage.ApiStandardVersion> = httpClient.getBody {
url {
appendEncodedPathSegments("package")
parameters {
append(hashingAlgorithm.name.lowercase(), hash)
}
}
}
suspend fun packageById(id: String): ApiPackageResponse<ApiStandardPackage, ApiStandardPackage.ApiStandardVersion> =
httpClient.getBody {
url {
appendEncodedPathSegments("package", id)
}
}
suspend fun readmeByPackageId(id: String): String =
httpClient.getBody {
url {
appendEncodedPathSegments("package", id, "readme")
}
accept(ContentType.Text.Html)
}
suspend fun statistics(): ApiStatisticsResponse =
httpClient.getBody {
url {
appendEncodedPathSegments("statistics")
}
}
suspend fun repositories(): ApiRepositoriesResponse =
httpClient.getBody {
url {
appendEncodedPathSegments("repositories")
}
}
override fun close() = httpClient.close()
}

View File

@@ -0,0 +1,111 @@
package org.jetbrains.idea.packagesearch.api
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ApplicationInfo
import com.intellij.openapi.components.Service
import io.ktor.client.engine.java.Java
import io.ktor.client.plugins.DefaultRequest
import io.ktor.client.plugins.HttpTimeout
import io.ktor.client.plugins.UserAgent
import io.ktor.client.plugins.logging.LogLevel
import io.ktor.client.plugins.logging.Logger
import io.ktor.client.plugins.logging.Logging
import io.ktor.client.request.headers
import org.jetbrains.annotations.ApiStatus.ScheduledForRemoval
import org.jetbrains.idea.maven.onlinecompletion.model.MavenDependencyCompletionItem
import org.jetbrains.idea.maven.onlinecompletion.model.MavenRepositoryArtifactInfo
import org.jetbrains.idea.reposearch.DependencySearchProvider
import org.jetbrains.idea.reposearch.PluginEnvironment
import org.jetbrains.idea.reposearch.RepositoryArtifactData
import org.jetbrains.idea.reposearch.logTrace
import org.jetbrains.packagesearch.api.v3.ApiMavenPackage
import org.jetbrains.packagesearch.api.v3.http.PackageSearchApiClient
import org.jetbrains.packagesearch.api.v3.http.PackageSearchApiClient.Companion.defaultHttpClient
import org.jetbrains.packagesearch.api.v3.http.PackageSearchEndpoints
import org.jetbrains.packagesearch.api.v3.http.searchPackages
import org.jetbrains.packagesearch.api.v3.search.jvmMavenPackages
import kotlin.time.Duration.Companion.seconds
@Service(Service.Level.APP)
class PackageSearchApiClientService : Disposable, DependencySearchProvider {
private val httpClient = defaultHttpClient(engine = Java, protobuf = false) {
install(UserAgent) {
agent = ApplicationInfo.getInstance().fullVersion
}
install(DefaultRequest) {
headers {
append("Api-Version", "3.1.1")
append("JB-IDE-Version", PluginEnvironment.ideVersion)
}
}
install(Logging) {
level = LogLevel.HEADERS
logger = object : Logger {
override fun log(message: String) {
logTrace(message)
}
}
}
install(HttpTimeout) {
requestTimeoutMillis = 10.seconds.inWholeMilliseconds
}
}
val client = PackageSearchApiClient(
endpoints = PackageSearchEndpoints.PROD,
httpClient = httpClient
)
override fun dispose() {
httpClient.close()
}
@Deprecated("Use directly the client instead")
@ScheduledForRemoval
suspend fun searchByString(searchString: String) =
client.searchPackages {
searchQuery = searchString
packagesType {
jvmMavenPackages()
}
}.filterIsInstance<ApiMavenPackage>()
@Deprecated("Use directly the client instead (PackageSearcgApiClientService)")
@ScheduledForRemoval
override suspend fun fulltextSearch(searchString: String): List<RepositoryArtifactData> =
client.searchPackages {
searchQuery = searchString
packagesType {
jvmMavenPackages()
}
}
.filterIsInstance<ApiMavenPackage>()
.map { it.repositoryArtifactData() }
@Deprecated("Use directly the client instead")
@ScheduledForRemoval
override suspend fun suggestPrefix(groupId: String, artifactId: String) =
fulltextSearch("$groupId:$artifactId")
override fun isLocal() = false
override val cacheKey: String = "PackageSearchProvider"
}
private fun ApiMavenPackage.repositoryArtifactData(): MavenRepositoryArtifactInfo {
val versions = versions.all.map {
MavenDependencyCompletionItem(
/* groupId = */ groupId,
/* artifactId = */ artifactId,
/* version = */ it.normalizedVersion.versionName,
/* type = */ MavenDependencyCompletionItem.Type.REMOTE
)
}
return MavenRepositoryArtifactInfo(
/* groupId = */ groupId,
/* artifactId = */ artifactId,
/* version = */ versions.toTypedArray()
)
}

View File

@@ -1,83 +0,0 @@
/*
* Copyright 2000-2022 JetBrains s.r.o. and contributors.
*
* 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.
*/
package org.jetbrains.idea.packagesearch.api
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import io.ktor.client.engine.HttpClientEngine
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import org.jetbrains.idea.maven.onlinecompletion.model.MavenDependencyCompletionItem
import org.jetbrains.idea.maven.onlinecompletion.model.MavenRepositoryArtifactInfo
import org.jetbrains.idea.packagesearch.DefaultPackageServiceConfig
import org.jetbrains.idea.packagesearch.PackageSearchServiceConfig
import org.jetbrains.idea.reposearch.DependencySearchProvider
import org.jetbrains.idea.reposearch.RepositoryArtifactData
import org.jetbrains.packagesearch.api.v2.ApiStandardPackage
/**
* If you need to access Package Search API only, it is generally recommended to use [PackageSearchApiClient] or [AsyncPackageSearchApiClient] directly.
*
* This class is needed to support the [DependencySearchProvider] interface, used by Maven plugin.
*/
class PackageSearchProvider(
private val scope: CoroutineScope,
config: PackageSearchServiceConfig = service<DefaultPackageServiceConfig>(),
engine: HttpClientEngine? = null
) : DependencySearchProvider {
private val myClient = PackageSearchApiClient(config, engine)
.also { client -> scope.coroutineContext[Job]?.invokeOnCompletion { client.close() } }
override suspend fun fulltextSearch(searchString: String): List<RepositoryArtifactData> {
return myClient.packagesByQuery(searchString)
.packages
.filter { it.groupId.isNotBlank() && it.artifactId.isNotBlank() }
.map { convertApiStandardPackage2RepositoryArtifactData(it) }
}
override suspend fun suggestPrefix(groupId: String, artifactId: String): List<RepositoryArtifactData> {
return myClient.suggestPackages(groupId, artifactId)
.packages
.filter { it.groupId.isNotBlank() && it.artifactId.isNotBlank() }
.map { convertApiStandardPackage2RepositoryArtifactData(it) }
}
override fun isLocal(): Boolean = false
override val cacheKey = "PackageSearchProvider" // assuming there's only one PackageSearchProvider per project
private fun convertApiStandardPackage2RepositoryArtifactData(pkg: ApiStandardPackage): RepositoryArtifactData {
val items = pkg.versions.map {
MavenDependencyCompletionItem(
pkg.groupId,
pkg.artifactId,
it.version,
MavenDependencyCompletionItem.Type.REMOTE)
}
return MavenRepositoryArtifactInfo(
pkg.groupId,
pkg.artifactId,
items.toTypedArray()
)
}
}
fun PackageSearchProvider(
project: Project,
config: PackageSearchServiceConfig = service<DefaultPackageServiceConfig>(),
engine: HttpClientEngine? = null
): PackageSearchProvider = PackageSearchProvider(project.service<LifecycleScope>().cs, config, engine)

View File

@@ -25,9 +25,6 @@
<registryKey key="packagesearch.config.url" defaultValue="https://resources.jetbrains.com/package-search/config/config.json"
description="package search service config url" restartRequired="true"/>
<registryKey key="packagesearch.timeout" defaultValue="15000"
description="timeout when package search service request failed (milliseconds)" restartRequired="true"/>
<registryKey id="org.jetbrains.idea.reposearch.log.debug"
key="org.jetbrains.idea.reposearch.log.debug"
defaultValue="false"

View File

@@ -1,98 +0,0 @@
/*
* Copyright 2000-2022 JetBrains s.r.o. and contributors.
*
* 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.
*/
package org.jetbrains.idea.reposearch
import com.intellij.testFramework.assertInstanceOf
import io.ktor.client.engine.mock.MockEngine
import io.ktor.client.engine.mock.respond
import io.ktor.http.ContentType
import io.ktor.http.HttpHeaders
import io.ktor.http.Parameters
import io.ktor.http.headersOf
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.jetbrains.idea.maven.onlinecompletion.model.MavenRepositoryArtifactInfo
import org.jetbrains.idea.packagesearch.api.PackageSearch
import org.jetbrains.idea.packagesearch.api.PackageSearchProvider
import org.junit.Test
import org.junit.jupiter.api.Assertions.assertEquals
class PackageSearchProviderTest {
private fun <T : Any> T.getResourceText(path: String) = this::class.java.classLoader
.getResourceAsStream(path)!!.bufferedReader().readText()
data class EngineInspector(val flow: Flow<Parameters>, val engine: MockEngine)
private fun getMockEngine(): EngineInspector {
val channel = Channel<Parameters>(Channel.UNLIMITED)
val engine = MockEngine { request ->
channel.send(request.url.parameters)
respond(
content = getResourceText("pkgs-response.json"),
headers = headersOf(
HttpHeaders.ContentType to listOf(ContentType.Application.PackageSearch.StandardV2.toString())
)
)
}
return EngineInspector(channel.consumeAsFlow(), engine)
}
private fun getClient(engine: MockEngine) = PackageSearchProvider(
scope = GlobalScope,
config = TestPackageSearchServiceConfig,
engine = engine
)
@Test
fun `test suggested packages search`() = runTest {
val (paramsFlow, engine) = getMockEngine()
val data = getClient(engine).suggestPrefix(
groupId = "org.apache.maven",
artifactId = "maven-plugin-api",
)
val params = paramsFlow.first()
assertEquals("org.apache.maven", params["groupid"])
assertEquals("maven-plugin-api", params["artifactid"])
assertEquals(1, data.size)
val info = assertInstanceOf<MavenRepositoryArtifactInfo>(data.first())
assertEquals("org.apache.maven", info.groupId)
assertEquals("maven-plugin-api", info.artifactId)
}
@Test
fun `test packages fulltext search`() = runTest {
val (paramsFlow, engine) = getMockEngine()
val data = getClient(engine).fulltextSearch(
searchString = "maven-plugin-api"
)
val params = paramsFlow.first()
assertEquals("maven-plugin-api", params["query"])
assertEquals(1, data.size)
val info = assertInstanceOf<MavenRepositoryArtifactInfo>(data.first())
assertEquals("org.apache.maven", info.groupId)
assertEquals("maven-plugin-api", info.artifactId)
}
}

View File

@@ -1,29 +0,0 @@
package org.jetbrains.idea.reposearch
import io.ktor.client.plugins.logging.LogLevel
import io.ktor.client.plugins.logging.Logger
import io.ktor.client.plugins.logging.SIMPLE
import io.ktor.http.Headers
import io.ktor.http.URLProtocol
import org.jetbrains.idea.packagesearch.PackageSearchServiceConfig
import kotlin.time.Duration.Companion.seconds
object TestPackageSearchServiceConfig : PackageSearchServiceConfig {
override val host = "test"
override val timeout
get() = 15.seconds
override val userAgent: String
get() = "TEST"
override val protocol = URLProtocol.HTTP
override val headers
get() = Headers.Empty
override val logLevel = LogLevel.ALL
override val logger = Logger.SIMPLE
}