@@ -9,13 +9,17 @@ import com.fasterxml.jackson.module.kotlin.readValue
99import com.intellij.openapi.Disposable
1010import com.intellij.openapi.application.ApplicationManager
1111import com.intellij.openapi.application.PathManager
12+ import com.intellij.openapi.components.PersistentStateComponent
1213import com.intellij.openapi.components.Service
1314import com.intellij.util.Alarm
1415import com.intellij.util.AlarmFactory
1516import com.intellij.util.io.HttpRequests
17+ import software.aws.toolkits.core.utils.RemoteResource
1618import software.aws.toolkits.core.utils.debug
1719import software.aws.toolkits.core.utils.error
1820import software.aws.toolkits.core.utils.getLogger
21+ import software.aws.toolkits.jetbrains.core.DefaultRemoteResourceResolverProvider
22+ import software.aws.toolkits.jetbrains.core.RemoteResourceResolverProvider
1923import java.io.IOException
2024import java.nio.file.Path
2125import 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