1+ /* ******************************************************************************
2+ * Copyright 2000-2016 JetBrains s.r.o.
3+ *
4+ * Licensed under the Apache License, Version 2.0 (the "License");
5+ * you may not use this file except in compliance with the License.
6+ * You may obtain a copy of the License at
7+ *
8+ * http://www.apache.org/licenses/LICENSE-2.0
9+ *
10+ * Unless required by applicable law or agreed to in writing, software
11+ * distributed under the License is distributed on an "AS IS" BASIS,
12+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+ * See the License for the specific language governing permissions and
14+ * limitations under the License.
15+ *
16+ *******************************************************************************/
17+ package org.jetbrains.kotlin.ui
18+
19+ import java.util.concurrent.TimeUnit
20+ import org.eclipse.core.runtime.IProgressMonitor
21+ import org.eclipse.equinox.p2.metadata.IInstallableUnit
22+ import org.eclipse.equinox.p2.operations.OperationFactory
23+ import org.eclipse.core.runtime.NullProgressMonitor
24+ import java.net.URI
25+ import org.eclipse.jface.dialogs.MessageDialog
26+ import org.eclipse.swt.widgets.Display
27+ import org.eclipse.equinox.p2.operations.ProfileModificationJob
28+ import org.eclipse.equinox.p2.operations.ProvisioningJob
29+ import org.eclipse.mylyn.commons.ui.dialogs.AbstractNotificationPopup
30+ import org.eclipse.swt.widgets.Composite
31+ import org.eclipse.swt.layout.GridLayout
32+ import org.eclipse.swt.widgets.Label
33+ import org.eclipse.swt.SWT
34+ import org.eclipse.swt.layout.GridData
35+ import org.eclipse.swt.widgets.Link
36+ import org.eclipse.swt.events.SelectionAdapter
37+ import org.eclipse.swt.events.SelectionEvent
38+ import org.eclipse.equinox.p2.operations.UpdateOperation
39+ import java.net.URL
40+ import java.net.URLConnection
41+ import java.net.HttpURLConnection
42+ import org.jetbrains.kotlin.core.log.KotlinLogger
43+ import com.intellij.openapi.util.SystemInfo
44+ import java.net.URLEncoder
45+ import org.eclipse.jface.preference.IPreferenceStore
46+ import org.eclipse.equinox.p2.metadata.Version
47+ import java.util.Random
48+ import org.jetbrains.kotlin.eclipse.ui.utils.runJob
49+ import org.eclipse.core.runtime.IStatus
50+ import org.eclipse.core.runtime.Status
51+ import java.io.IOException
52+ import org.eclipse.core.runtime.jobs.JobChangeAdapter
53+ import org.eclipse.core.runtime.jobs.IJobChangeEvent
54+ import org.eclipse.ui.PlatformUI
55+
56+ private sealed class PluginUpdateStatus () {
57+ object Update : PluginUpdateStatus()
58+
59+ object CheckUpdateSiteFailed : PluginUpdateStatus()
60+
61+ class UpdateFailed (val message : String ) : PluginUpdateStatus()
62+ }
63+
64+ public object KotlinPluginUpdater {
65+ val LAST_UPDATE_CHECK = " kotlin.lastUpdateCheck"
66+ val USER_ID = " kotlin.userId"
67+
68+ private val KOTLIN_GROUP_ID = " org.jetbrains.kotlin.feature.feature.group"
69+ private val ECLIPSE_PLATFORM_ID = " org.eclipse.platform"
70+
71+ private val MAIN_DELAY : Long = TimeUnit .DAYS .toMillis(1 )
72+ private val retryState = RetryState (TimeUnit .HOURS .toMillis(3 ))
73+
74+ fun kotlinFileEdited () {
75+ val kotlinStore = Activator .getDefault().preferenceStore
76+
77+ val updateStatus: PluginUpdateStatus
78+ if (retryState.retryToUpdate()) {
79+ updateStatus = tryToUpdate(kotlinStore)
80+ } else {
81+ val lastUpdateTime = kotlinStore.getLong(LAST_UPDATE_CHECK )
82+ if (lastUpdateTime == 0L || checkTimeIsUp(lastUpdateTime, MAIN_DELAY )) {
83+ updateStatus = tryToUpdate(kotlinStore)
84+ } else {
85+ return
86+ }
87+ }
88+
89+ processResults(updateStatus, kotlinStore)
90+ }
91+
92+ private fun getKotlinInstallationUnit (monitor : IProgressMonitor ): IInstallableUnit ? {
93+ return OperationFactory ().listInstalledElements(true , monitor).find { it.id == KOTLIN_GROUP_ID }
94+ }
95+
96+ private fun checkTimeIsUp (lastTry : Long , delay : Long ): Boolean = System .currentTimeMillis() - lastTry > delay
97+
98+ private fun processResults (updateStatus : PluginUpdateStatus , kotlinStore : IPreferenceStore ) {
99+ when (updateStatus) {
100+ is PluginUpdateStatus .Update -> {
101+ kotlinStore.setValue(LAST_UPDATE_CHECK , System .currentTimeMillis())
102+ retryState.disableRetry()
103+ }
104+
105+ is PluginUpdateStatus .CheckUpdateSiteFailed -> retryState.setUpForRetry()
106+
107+ is PluginUpdateStatus .UpdateFailed -> {
108+ KotlinLogger .logWarning(" Could not check for update (${updateStatus.message} )" )
109+ retryState.setUpForRetry()
110+ }
111+ }
112+ }
113+
114+ private fun tryToUpdate (store : IPreferenceStore ): PluginUpdateStatus {
115+ val monitor = NullProgressMonitor ()
116+ val kotlinUnit = getKotlinInstallationUnit(monitor)
117+ if (kotlinUnit == null ) {
118+ return PluginUpdateStatus .UpdateFailed (" There is no kotlin feature group with id: $KOTLIN_GROUP_ID " )
119+ }
120+
121+ val os = URLEncoder .encode(SystemInfo .OS_NAME + " " + SystemInfo .OS_VERSION , " UTF-8" )
122+ val pluginVersion = kotlinUnit.version
123+ val uid = getOrSetUID(store)
124+ val eclipseVersion = getEclipsePlatformVersion(monitor)
125+ if (eclipseVersion == null ) {
126+ KotlinLogger .logWarning(" There is no eclipse platform group with id: $ECLIPSE_PLATFORM_ID " )
127+ }
128+
129+ val params = " build=$eclipseVersion &pluginVersion=$pluginVersion &os=$os &uuid=$uid "
130+ val updateSiteStatus = obtainUpdateSite(" https://plugins.jetbrains.com/eclipse-plugins/kotlin/update?$params " )
131+
132+ return when (updateSiteStatus) {
133+ is UpdateSiteStatus .UpdateSite -> {
134+ val resolvedUpdateSite = updateSiteStatus.updateSite.removeSuffix(" compositeArtifacts.xml" )
135+ proposeToUpdate(kotlinUnit, resolvedUpdateSite, monitor)
136+ }
137+
138+ is UpdateSiteStatus .UpdateSiteNotFound -> PluginUpdateStatus .CheckUpdateSiteFailed
139+
140+ is UpdateSiteStatus .Failed -> PluginUpdateStatus .UpdateFailed (" Could not obtain update site" )
141+ }
142+ }
143+
144+ private fun proposeToUpdate (kotlinUnit : IInstallableUnit , updateSite : String , monitor : IProgressMonitor ): PluginUpdateStatus {
145+ val uri = URI (updateSite)
146+ val updateOperation = OperationFactory ().createUpdateOperation(listOf (kotlinUnit), listOf (uri), monitor)
147+ val result = updateOperation.resolveModal(monitor)
148+ if (result.isOK) {
149+ Display .getDefault().asyncExec {
150+ val updateNotification = UpdatePluginNotification (updateOperation, monitor, Display .getDefault())
151+ updateNotification.open()
152+ }
153+ }
154+
155+ return PluginUpdateStatus .Update
156+ }
157+
158+ private fun getOrSetUID (store : IPreferenceStore ): Long {
159+ val userId = store.getLong(USER_ID )
160+ if (userId == 0L ) {
161+ val generatedId = Random ().nextLong()
162+ store.setValue(USER_ID , generatedId)
163+ return generatedId
164+ }
165+
166+ return userId
167+ }
168+
169+ private fun getEclipsePlatformVersion (monitor : IProgressMonitor ): Version ? {
170+ return OperationFactory ().listInstalledElements(false , monitor)
171+ .find { it.id == ECLIPSE_PLATFORM_ID }
172+ ?.version
173+ }
174+
175+ private fun obtainUpdateSite (urlString : String ): UpdateSiteStatus {
176+ val url = URL (urlString)
177+ val connection = url.openConnection()
178+ if (connection is HttpURLConnection ) {
179+ try {
180+ connection.setInstanceFollowRedirects(false )
181+ connection.connect()
182+
183+ if (connection.getResponseCode() == 302 ) {
184+ val redirect = connection.getHeaderField(" Location" )
185+ return if (redirect != null ) UpdateSiteStatus .UpdateSite (redirect) else UpdateSiteStatus .Failed
186+ }
187+ } catch (e: IOException ) {
188+ return UpdateSiteStatus .UpdateSiteNotFound
189+ } finally {
190+ connection.disconnect()
191+ }
192+ }
193+
194+ return UpdateSiteStatus .Failed
195+ }
196+
197+ private sealed class UpdateSiteStatus {
198+ object UpdateSiteNotFound : UpdateSiteStatus()
199+
200+ object Failed : UpdateSiteStatus()
201+
202+ class UpdateSite (val updateSite : String ) : UpdateSiteStatus()
203+ }
204+
205+ private class RetryState (private val retryDelay : Long ) {
206+ @Volatile private var lastRetryTime: Long = 0
207+ @Volatile private var isActive: Boolean = false
208+
209+ @Synchronized fun setUpForRetry () {
210+ isActive = true
211+ lastRetryTime = System .currentTimeMillis()
212+ }
213+
214+ fun disableRetry () {
215+ isActive = false
216+ }
217+
218+ fun retryToUpdate (): Boolean {
219+ return isActive && checkTimeIsUp(lastRetryTime, retryDelay)
220+ }
221+ }
222+ }
223+
224+ private class UpdatePluginNotification (
225+ val updateOperation : UpdateOperation ,
226+ val monitor : IProgressMonitor ,
227+ val display : Display ) : AbstractNotificationPopup(display) {
228+ init {
229+ setDelayClose(0 ) // Don't close popup automatically
230+ }
231+
232+ override fun createContentArea (parent : Composite ) {
233+ parent.setLayout(GridLayout (1 , true ))
234+
235+ val textLabel = Label (parent, SWT .LEFT )
236+ val installbleUnit = getInstallableUnit()
237+ textLabel.setText(" A new version ${installbleUnit.version} of the Kotlin plugin is available." )
238+ textLabel.setLayoutData(gridData())
239+
240+ val updateLink = Link (parent, SWT .RIGHT )
241+ updateLink.setText(" <a>Update Plugin</a>" )
242+ updateLink.setLayoutData(gridData(verticalAlignment = SWT .BOTTOM , grabExcessVerticalSpace = true ))
243+
244+ updateLink.addSelectionListener(object : SelectionAdapter () {
245+ override fun widgetSelected (e : SelectionEvent ) {
246+ val job = updateOperation.getProvisioningJob(monitor)
247+ job.addJobChangeListener(RestartJobAdapter (display))
248+ job.schedule()
249+
250+ this @UpdatePluginNotification.close()
251+ }
252+ })
253+ }
254+
255+ override fun getPopupShellTitle (): String = " Kotlin Plugin"
256+
257+ private fun getInstallableUnit (): IInstallableUnit {
258+ return updateOperation.getProfileChangeRequest().getAdditions().first()
259+ }
260+ }
261+
262+ private fun gridData (
263+ horizontalAlignment : Int = SWT .BEGINNING ,
264+ verticalAlignment : Int = SWT .CENTER ,
265+ grabExcessHorizontalSpace : Boolean = false,
266+ grabExcessVerticalSpace : Boolean = false,
267+ horizontalSpan : Int = 1,
268+ verticalSpan : Int = 1): GridData {
269+ return GridData (horizontalAlignment, verticalAlignment, grabExcessHorizontalSpace, grabExcessVerticalSpace, horizontalSpan, verticalSpan)
270+ }
271+
272+ private class RestartJobAdapter (val display : Display ) : JobChangeAdapter() {
273+ override fun done (event : IJobChangeEvent ) {
274+ val result = event.result
275+ if (result.isOK) {
276+ display.syncExec {
277+ val restart = MessageDialog .openQuestion(
278+ null ,
279+ " Updates installed, restart?" ,
280+ " Updates have been installed successfully, do you want to restart?" );
281+ if (restart) {
282+ PlatformUI .getWorkbench().restart()
283+ }
284+ }
285+ } else if (! result.matches(IStatus .CANCEL )) {
286+ display.syncExec {
287+ MessageDialog .openError(null , " Error" , event.result.message)
288+ }
289+ }
290+ }
291+ }
0 commit comments