Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
c4aa8cb
Added Manifest Fetcher
LokeshDogga13 Feb 18, 2025
d1ca7d1
Addressing code review comments
LokeshDogga13 Feb 18, 2025
600f91a
Added unit test cases
LokeshDogga13 Feb 18, 2025
cad10ef
Fixing lint issues
LokeshDogga13 Feb 18, 2025
2607eec
Merge branch 'feature/q-lsp' into lodogga/initialChanges
LokeshDogga13 Feb 19, 2025
b0e9648
Addressing code review comments
LokeshDogga13 Feb 19, 2025
973b710
Addressing code review comments
LokeshDogga13 Feb 19, 2025
2641472
Merge branch 'feature/q-lsp' into lodogga/initialChanges
LokeshDogga13 Feb 19, 2025
fef3b06
Fixing lint issues
LokeshDogga13 Feb 19, 2025
539cb7e
Merge remote-tracking branch 'origin/lodogga/initialChanges' into lod…
LokeshDogga13 Feb 19, 2025
563ca08
Addressing code review comments
LokeshDogga13 Feb 19, 2025
8f44478
Fixing detektMain lint issues
LokeshDogga13 Feb 19, 2025
30f8dc6
Added unit test cases
LokeshDogga13 Feb 21, 2025
eef3e59
Merge branch 'feature/q-lsp' into lodogga/initialChanges
LokeshDogga13 Feb 21, 2025
bc25fb9
Updating code according to spec.
LokeshDogga13 Feb 24, 2025
7ac76b0
detekt
LokeshDogga13 Feb 24, 2025
a28dca7
Merge branch 'feature/q-lsp' into lodogga/initialChanges
LokeshDogga13 Feb 24, 2025
20e7451
Fixing typo
LokeshDogga13 Feb 24, 2025
a970509
Artifact changes
LokeshDogga13 Feb 25, 2025
44f5c7b
Fixing validation function
LokeshDogga13 Feb 26, 2025
60c4b1c
Merge branch 'feature/q-lsp' into lodogga/initialChanges
LokeshDogga13 Feb 26, 2025
65f6f68
Addressing code review comments
LokeshDogga13 Feb 26, 2025
224289d
Merge branch 'feature/q-lsp' into lodogga/initialChanges
LokeshDogga13 Feb 26, 2025
3e1bc1e
Fixing Detekt
LokeshDogga13 Feb 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts

import io.mockk.every
import io.mockk.spyk
import io.mockk.verify
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import software.aws.toolkits.jetbrains.services.amazonq.project.manifest.ManifestManager

class ManifestFetcherTest {

private lateinit var manifestFetcher: ManifestFetcher
private lateinit var manifest: ManifestManager.Manifest

@BeforeEach
fun setup() {
manifestFetcher = ManifestFetcher()
manifest = ManifestManager.Manifest()
}

@Test
fun `should return null when both local and remote manifests are null`() {
val fetchLocalManifestMock = spyk<ManifestFetcher>(recordPrivateCalls = true)

every { fetchLocalManifestMock["fetchManifestFromLocal"]() } returns null
every { fetchLocalManifestMock["fetchManifestFromRemote"]() } returns null

assertEquals(fetchLocalManifestMock.fetch(), null)
verify { fetchLocalManifestMock["fetchManifestFromLocal"]() }
verify { fetchLocalManifestMock["fetchManifestFromRemote"]() }
}

@Test
fun `should return valid result from local should not execute remote method`() {
val fetchLocalManifestMock = spyk<ManifestFetcher>(recordPrivateCalls = true)

every { fetchLocalManifestMock["fetchManifestFromLocal"]() } returns manifest
every { fetchLocalManifestMock["fetchManifestFromRemote"]() } returns null

assertEquals(fetchLocalManifestMock.fetch(), manifest)
verify { fetchLocalManifestMock["fetchManifestFromLocal"]() }
}

@Test
fun `should return valid result from remote`() {
val fetchLocalManifestMock = spyk<ManifestFetcher>(recordPrivateCalls = true)

every { fetchLocalManifestMock["fetchManifestFromLocal"]() } returns null
every { fetchLocalManifestMock["fetchManifestFromRemote"]() } returns manifest

assertEquals(fetchLocalManifestMock.fetch(), manifest)
verify { fetchLocalManifestMock["fetchManifestFromLocal"]() }
verify { fetchLocalManifestMock["fetchManifestFromRemote"]() }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts

import com.intellij.openapi.util.SystemInfo
import java.nio.file.Path
import java.nio.file.Paths

fun getToolkitsCommonCachePath(): Path = when {
SystemInfo.isWindows -> {
Paths.get(System.getenv("APPDATA"))
}
SystemInfo.isMac -> {
Paths.get(System.getProperty("user.home"), "Library", "Caches")
}
else -> {
Paths.get(System.getProperty("user.home"), ".cache")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts

import com.intellij.openapi.util.text.StringUtil
import com.intellij.util.io.DigestUtil
import software.aws.toolkits.core.utils.exists
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.readText
import software.aws.toolkits.jetbrains.core.getETagFromUrl
import software.aws.toolkits.jetbrains.core.getTextFromUrl
import software.aws.toolkits.jetbrains.core.saveFileFromUrl
import software.aws.toolkits.jetbrains.services.amazonq.project.manifest.ManifestManager
import java.nio.file.Path

class ManifestFetcher {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we replace the existing implementation with this?


private val lspManifestUrl = "https://aws-toolkit-language-servers.amazonaws.com/codewhisperer/0/manifest.json"
private val manifestManager = ManifestManager()
private val lspManifestFilePath: Path = getToolkitsCommonCachePath().resolve("aws").resolve("toolkits").resolve("language-servers")
.resolve("lsp-manifest.json")

companion object {
private val logger = getLogger<ManifestFetcher>()
}

/**
* Method which will be used to fetch latest manifest.
* */
fun fetch(): ManifestManager.Manifest? {
val localManifest = fetchManifestFromLocal()
if (localManifest != null) {
return localManifest
}
val remoteManifest = fetchManifestFromRemote() ?: return null
return remoteManifest
}

private fun fetchManifestFromRemote(): ManifestManager.Manifest? {
val manifest: ManifestManager.Manifest?
try {
val manifestString = getTextFromUrl(lspManifestUrl)
manifest = manifestManager.readManifestFile(manifestString) ?: return null
} catch (e: Exception) {
logger.error("error fetching lsp manifest from remote URL ${e.message}", e)
return null
}
if (manifest.isManifestDeprecated == true) {
logger.info("Manifest is deprecated")
return null
}
updateManifestCache()
logger.info("Using manifest found from remote URL")
return manifest
}

private fun updateManifestCache() {
try {
saveFileFromUrl(lspManifestUrl, lspManifestFilePath)
} catch (e: Exception) {
logger.error("error occurred while saving lsp manifest to local cache ${e.message}", e)
}
}

private fun fetchManifestFromLocal(): ManifestManager.Manifest? {
val localETag = getManifestETagFromLocal()
val remoteETag = getManifestETagFromUrl()
// If local and remote have same ETag, we can re-use the manifest file from local to fetch artifacts.
if (localETag != null && remoteETag != null && localETag == remoteETag) {
try {
val manifestContent = lspManifestFilePath.readText()
return manifestManager.readManifestFile(manifestContent)
} catch (e: Exception) {
logger.error("error reading lsp manifest file from local ${e.message}", e)
return null
}
}
return null
}

private fun getManifestETagFromLocal(): String? {
if (lspManifestFilePath.exists()) {
val messageDigest = DigestUtil.md5()
DigestUtil.updateContentHash(messageDigest, lspManifestFilePath)
return StringUtil.toHexString(messageDigest.digest())
}
return null
}

private fun getManifestETagFromUrl(): String? {
try {
val actualETag = getETagFromUrl(lspManifestUrl)
return actualETag.trim('"')
} catch (e: Exception) {
logger.error("error fetching ETag of lsp manifest from url.", e)
}
return null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@

package software.aws.toolkits.jetbrains.services.amazonq.project.manifest

import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import com.intellij.openapi.util.SystemInfo
Expand All @@ -18,10 +18,9 @@ class ManifestManager {
val currentVersion = "0.1.32"
val currentOs = getOs()
private val arch = CpuArch.CURRENT
private val mapper = jacksonObjectMapper()
private val mapper = jacksonObjectMapper().apply { configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) }

data class TargetContent(
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonProperty("filename")
val filename: String? = null,
@JsonProperty("url")
Expand All @@ -33,16 +32,15 @@ class ManifestManager {
)

data class VersionTarget(
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonProperty("platform")
val platform: String? = null,
@JsonProperty("arch")
val arch: String? = null,
@JsonProperty("contents")
val contents: List<TargetContent>? = emptyList(),
)

data class Version(
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonProperty("serverVersion")
val serverVersion: String? = null,
@JsonProperty("isDelisted")
Expand All @@ -52,7 +50,6 @@ class ManifestManager {
)

data class Manifest(
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonProperty("manifestSchemaVersion")
val manifestSchemaVersion: String? = null,
@JsonProperty("artifactId")
Expand All @@ -67,7 +64,7 @@ class ManifestManager {

fun getManifest(): Manifest? = fetchFromRemoteAndSave()

private fun readManifestFile(content: String): Manifest? {
fun readManifestFile(content: String): Manifest? {
try {
return mapper.readValue<Manifest>(content)
} catch (e: Exception) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
package software.aws.toolkits.jetbrains.core

import com.intellij.openapi.application.PathManager
import com.intellij.util.io.HttpRequests
import com.intellij.util.io.createDirectories
import software.aws.toolkits.core.utils.DefaultRemoteResourceResolver
import software.aws.toolkits.core.utils.UrlFetcher
Expand Down Expand Up @@ -41,11 +40,7 @@ class DefaultRemoteResourceResolverProvider : RemoteResourceResolverProvider {
}

override fun getETag(url: String): String =
HttpRequests.head(url)
.userAgent("AWS Toolkit for JetBrains")
.connect { request ->
request.connection.headerFields["ETag"]?.firstOrNull().orEmpty()
}
getETagFromUrl(url)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,10 @@ fun writeJsonToUrl(url: String, jsonString: String, indicator: ProgressIndicator
request.write(jsonString)
request.readString(indicator)
}

fun getETagFromUrl(url: String): String =
HttpRequests.head(url)
.userAgent(AwsClientManager.getUserAgent())
.connect { request ->
request.connection.headerFields["ETag"]?.firstOrNull().orEmpty()
}
Loading