Skip to content

Commit c4f3cac

Browse files
committed
remote resource implementation
1 parent 1d6e7d2 commit c4f3cac

File tree

1 file changed

+35
-117
lines changed

1 file changed

+35
-117
lines changed

plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/notifications/NotificationPollingService.kt

Lines changed: 35 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,17 @@ import com.fasterxml.jackson.module.kotlin.readValue
99
import com.intellij.openapi.Disposable
1010
import com.intellij.openapi.application.ApplicationManager
1111
import com.intellij.openapi.application.PathManager
12+
import com.intellij.openapi.components.PersistentStateComponent
1213
import com.intellij.openapi.components.Service
1314
import com.intellij.util.Alarm
1415
import com.intellij.util.AlarmFactory
1516
import com.intellij.util.io.HttpRequests
17+
import software.aws.toolkits.core.utils.RemoteResource
1618
import software.aws.toolkits.core.utils.debug
1719
import software.aws.toolkits.core.utils.error
1820
import software.aws.toolkits.core.utils.getLogger
21+
import software.aws.toolkits.jetbrains.core.DefaultRemoteResourceResolverProvider
22+
import software.aws.toolkits.jetbrains.core.RemoteResourceResolverProvider
1923
import java.io.IOException
2024
import java.nio.file.Path
2125
import java.time.Duration
@@ -35,12 +39,32 @@ interface NotificationPollingService {
3539
}
3640

3741
@Service(Service.Level.APP)
38-
class NotificationPollingServiceImpl : NotificationPollingService, Disposable {
42+
class NotificationPollingServiceImpl :
43+
NotificationPollingService,
44+
PersistentStateComponent<NotificationPollingServiceImpl.State>,
45+
Disposable {
3946

47+
private var state = State()
4048
private val mapper = jacksonObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
4149
private var currentETag: String? = null // todo Persistant state? add an init possibly
4250
private val alarm = AlarmFactory.getInstance().create(Alarm.ThreadToUse.POOLED_THREAD, this)
4351
private val pollingIntervalMs = Duration.ofMinutes(10).toMillis()
52+
private val resourceResolver: RemoteResourceResolverProvider = DefaultRemoteResourceResolverProvider()
53+
private val notificationsResource = object : RemoteResource {
54+
override val name: String = NOTIFICATIONS_RESOURCE_PATH
55+
override val urls: List<String> = listOf(NOTIFICATION_ENDPOINT)
56+
}
57+
58+
// PersistentStateComponent implementation
59+
data class State(
60+
var currentETag: String? = null,
61+
)
62+
63+
override fun getState(): State = state
64+
65+
override fun loadState(state: State) {
66+
this.state = state
67+
}
4468

4569
override fun dispose() {
4670
alarm.dispose()
@@ -59,24 +83,26 @@ class NotificationPollingServiceImpl : NotificationPollingService, Disposable {
5983
* Main polling function that checks for updates and downloads if necessary
6084
* Returns the parsed notifications if successful, null otherwise
6185
*/
62-
private fun pollForNotifications(): NotificationFile? {
86+
private fun pollForNotifications(): Boolean {
6387
var retryCount = 0
6488
var lastException: Exception? = null
89+
// check ETag of current cached file
6590

6691
while (retryCount < MAX_RETRIES) {
6792
try {
6893
// Check if there are updates available
6994
val newETag = getNotificationETag()
70-
7195
if (newETag == currentETag) {
7296
LOG.debug { "No updates available for notifications" }
73-
return loadLocalNotifications()
97+
return false
7498
}
75-
76-
// Download and process new notifications
77-
val notifications = downloadAndProcessNotifications()
99+
// Force a new download by resolving the resource
100+
resourceResolver.get()
101+
.resolve(notificationsResource)
102+
.toCompletableFuture()
103+
.get()
78104
currentETag = newETag
79-
return notifications
105+
return true
80106
} catch (e: Exception) {
81107
lastException = e
82108
LOG.error(e) { "Failed to poll for notifications (attempt ${retryCount + 1}/$MAX_RETRIES)" }
@@ -87,10 +113,8 @@ class NotificationPollingServiceImpl : NotificationPollingService, Disposable {
87113
}
88114
}
89115
}
90-
91-
// After all retries failed, emit metric and return null
92116
emitFailureMetric(lastException)
93-
return loadLocalNotifications()
117+
return false
94118
}
95119

96120
private fun getNotificationETag(): String =
@@ -100,85 +124,6 @@ class NotificationPollingServiceImpl : NotificationPollingService, Disposable {
100124
request.connection.headerFields["ETag"]?.firstOrNull() ?: ""
101125
}
102126

103-
private fun downloadAndProcessNotifications(): NotificationFile {
104-
val content = HttpRequests.request(NOTIFICATION_ENDPOINT)
105-
.userAgent("AWS Toolkit for JetBrains")
106-
.readString()
107-
108-
// Save to local file for backup
109-
saveLocalNotifications(content)
110-
111-
return deserializeNotifications(content)
112-
}
113-
114-
/**
115-
* Deserializes the notification content with error handling
116-
*/
117-
private fun deserializeNotifications(content: String): NotificationFile {
118-
try {
119-
return mapper.readValue(content)
120-
} catch (e: Exception) {
121-
LOG.error(e) { "Failed to deserialize notifications" }
122-
throw e
123-
}
124-
}
125-
126-
/**
127-
* Loads notifications, preferring the latest downloaded version if available,
128-
* falling back to bundled resource if no downloaded version exists
129-
*/
130-
private fun loadLocalNotifications(): NotificationFile? {
131-
// First try to load from system directory (latest downloaded version)
132-
getLocalNotificationsPath().let { path ->
133-
if (path.exists()) {
134-
try {
135-
val content = path.readText()
136-
return deserializeNotifications(content)
137-
} catch (e: Exception) {
138-
LOG.error(e) { "Failed to load downloaded notifications, falling back to bundled resource" }
139-
}
140-
}
141-
}
142-
143-
// Fall back to bundled resource if no downloaded version exists
144-
return try {
145-
val resourceStream = javaClass.getResourceAsStream(NOTIFICATIONS_RESOURCE_PATH)
146-
?: return null
147-
148-
val content = resourceStream.use { stream ->
149-
stream.bufferedReader().readText()
150-
}
151-
152-
deserializeNotifications(content)
153-
} catch (e: Exception) {
154-
LOG.error(e) { "Failed to load notifications from bundled resources" }
155-
null
156-
}
157-
}
158-
159-
/**
160-
* Saves downloaded notifications to system directory
161-
*/
162-
private fun saveLocalNotifications(content: String) {
163-
try {
164-
val path = getLocalNotificationsPath()
165-
path.parent.createDirectories()
166-
path.writeText(content)
167-
} catch (e: IOException) {
168-
LOG.error(e) { "Failed to save notifications to local storage" }
169-
}
170-
}
171-
172-
/**
173-
* Gets the path for downloaded notifications in IntelliJ's system directory
174-
*/
175-
private fun getLocalNotificationsPath(): Path {
176-
return Path.of(PathManager.getSystemPath())
177-
.resolve("aws-toolkit")
178-
.resolve("notifications")
179-
.resolve("notifications.json")
180-
}
181-
182127
/**
183128
* Emits telemetry metric for polling failures
184129
*/
@@ -193,30 +138,3 @@ class NotificationPollingServiceImpl : NotificationPollingService, Disposable {
193138
ApplicationManager.getApplication().getService(NotificationPollingService::class.java)
194139
}
195140
}
196-
197-
data class NotificationFile(
198-
val notifications: List<Notification>,
199-
val version: String,
200-
)
201-
202-
data class Notification(
203-
val id: String,
204-
val message: String,
205-
val criteria: NotificationCriteria,
206-
)
207-
208-
data class NotificationCriteria(
209-
val minVersion: String?,
210-
val maxVersion: String?,
211-
val regions: List<String>?,
212-
val ideType: String?,
213-
val pluginVersion: String?,
214-
val os: String?,
215-
val authType: String?,
216-
val authRegion: String?,
217-
val authState: String?,
218-
val authScopes: List<String>?,
219-
val installedPlugins: List<String>?,
220-
val computeEnvironment: String?,
221-
val messageType: String?,
222-
)

0 commit comments

Comments
 (0)