IDEA-246851 - Add action to disable maven wrapper
[idea/community.git] / plugins / maven / src / main / java / org / jetbrains / idea / maven / buildtool / MavenSyncConsole.kt
1 // Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
2 package org.jetbrains.idea.maven.buildtool
3
4 import com.intellij.build.*
5 import com.intellij.build.events.EventResult
6 import com.intellij.build.events.MessageEvent
7 import com.intellij.build.events.MessageEventResult
8 import com.intellij.build.events.impl.*
9 import com.intellij.build.issue.BuildIssue
10 import com.intellij.build.issue.BuildIssueQuickFix
11 import com.intellij.build.process.BuildProcessHandler
12 import com.intellij.icons.AllIcons
13 import com.intellij.openapi.actionSystem.AnAction
14 import com.intellij.openapi.actionSystem.AnActionEvent
15 import com.intellij.openapi.components.ServiceManager
16 import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskId
17 import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskType
18 import com.intellij.openapi.progress.EmptyProgressIndicator
19 import com.intellij.openapi.progress.ProgressIndicator
20 import com.intellij.openapi.project.Project
21 import com.intellij.openapi.util.registry.Registry
22 import com.intellij.openapi.util.text.StringUtil
23 import com.intellij.openapi.vfs.VirtualFile
24 import com.intellij.pom.Navigatable
25 import com.intellij.util.ExceptionUtil
26 import org.jetbrains.annotations.ApiStatus
27 import org.jetbrains.annotations.Nls
28 import org.jetbrains.idea.maven.buildtool.quickfix.OffMavenOfflineModeQuickFix
29 import org.jetbrains.idea.maven.buildtool.quickfix.OpenMavenSettingsQuickFix
30 import org.jetbrains.idea.maven.buildtool.quickfix.UseBundledMavenQuickFix
31 import org.jetbrains.idea.maven.execution.SyncBundle
32 import org.jetbrains.idea.maven.project.MavenProjectsManager
33 import org.jetbrains.idea.maven.project.MavenWorkspaceSettingsComponent
34 import org.jetbrains.idea.maven.server.MavenServerManager
35 import org.jetbrains.idea.maven.server.MavenServerProgressIndicator
36 import org.jetbrains.idea.maven.utils.MavenLog
37 import org.jetbrains.idea.maven.utils.MavenUtil
38 import java.io.File
39 import java.io.OutputStream
40 import javax.swing.JComponent
41
42 class MavenSyncConsole(private val myProject: Project) {
43   @Volatile
44   private var mySyncView: BuildProgressListener = BuildProgressListener { _, _ -> }
45   private var mySyncId = ExternalSystemTaskId.create(MavenUtil.SYSTEM_ID, ExternalSystemTaskType.RESOLVE_PROJECT, myProject)
46   private var finished = false
47   private var started = false
48   private var wrapperProgressIndicator: WrapperProgressIndicator? = null
49   private var hasErrors = false
50   private var hasUnresolved = false
51   private val JAVADOC_AND_SOURCE_CLASSIFIERS = setOf("javadoc", "sources", "test-javadoc", "test-sources")
52   private val delayedActions = ArrayList<() -> Unit>()
53
54   private var myStartedSet = LinkedHashSet<Pair<Any, String>>()
55
56   @Synchronized
57   fun startImport(syncView: BuildProgressListener) {
58     if (started) {
59       return
60     }
61     val restartAction: AnAction = object : AnAction() {
62       override fun update(e: AnActionEvent) {
63         e.presentation.isEnabled = !started || finished
64         e.presentation.icon = AllIcons.Actions.Refresh
65       }
66
67       override fun actionPerformed(e: AnActionEvent) {
68         e.project?.let {
69           MavenProjectsManager.getInstance(it).forceUpdateAllProjectsOrFindAllAvailablePomFiles()
70         }
71       }
72     }
73     started = true
74     finished = false
75     hasErrors = false
76     hasUnresolved = false
77     wrapperProgressIndicator = WrapperProgressIndicator()
78     mySyncView = syncView
79     mySyncId = ExternalSystemTaskId.create(MavenUtil.SYSTEM_ID, ExternalSystemTaskType.RESOLVE_PROJECT, myProject)
80     val runDescr = BuildContentDescriptor(null, null, object : JComponent() {}, SyncBundle.message("maven.sync.title"))
81     runDescr.isActivateToolWindowWhenFailed = true
82     runDescr.isActivateToolWindowWhenAdded = false
83
84     val descriptor = DefaultBuildDescriptor(mySyncId, SyncBundle.message("maven.sync.title"), myProject.basePath!!,
85                                             System.currentTimeMillis())
86       .withRestartAction(restartAction)
87       .withContentDescriptor{runDescr}
88
89     mySyncView.onEvent(mySyncId, StartBuildEventImpl(descriptor, SyncBundle.message("maven.sync.project.title", myProject.name)))
90     debugLog("maven sync: started importing $myProject")
91   }
92
93   @Synchronized
94   fun addText(text: String) = doIfImportInProcess {
95     addText(mySyncId, text, true)
96   }
97
98   @Synchronized
99   private fun addText(parentId: Any, text: String, stdout: Boolean) = doIfImportInProcess {
100     if (StringUtil.isEmpty(text)) {
101       return
102     }
103     val toPrint = if (text.endsWith('\n')) text else "$text\n"
104     mySyncView.onEvent(mySyncId, OutputBuildEventImpl(parentId, toPrint, stdout))
105   }
106
107   @Synchronized
108   fun addWarning(@Nls text: String, @Nls description: String) = doIfImportInProcess {
109     mySyncView.onEvent(mySyncId,
110                        MessageEventImpl(mySyncId, MessageEvent.Kind.WARNING, SyncBundle.message("maven.sync.group.compiler"), text,
111                                         description))
112   }
113
114   @Synchronized
115   fun finishImport() {
116     debugLog("Maven sync: finishImport")
117     doFinish()
118   }
119
120
121   @Synchronized
122   fun terminated(exitCode: Int) = doIfImportInProcess {
123     val tasks = myStartedSet.toList().asReversed()
124     debugLog("Tasks $tasks are not completed! Force complete")
125     tasks.forEach { completeTask(it.first, it.second, FailureResultImpl(SyncBundle.message("maven.sync.failure.terminated", exitCode))) }
126
127     mySyncView.onEvent(mySyncId, FinishBuildEventImpl(mySyncId, null, System.currentTimeMillis(), "",
128                                                       FailureResultImpl(SyncBundle.message("maven.sync.failure.terminated", exitCode))))
129     finished = true
130     started = false
131
132   }
133
134   @Synchronized
135   fun startWrapperResolving() {
136     if (!started || finished) {
137       startImport(ServiceManager.getService(myProject, SyncViewManager::class.java))
138     }
139     startTask(mySyncId, SyncBundle.message("maven.sync.wrapper"))
140   }
141
142   @Synchronized
143   fun finishWrapperResolving(e: Throwable? = null) {
144     if (e != null) {
145       addWarning(SyncBundle.message("maven.sync.wrapper.failure"), e.localizedMessage)
146     }
147     completeTask(mySyncId, SyncBundle.message("maven.sync.wrapper"), SuccessResultImpl())
148   }
149
150   fun progressIndicatorForWrapper(): ProgressIndicator {
151     return wrapperProgressIndicator ?: EmptyProgressIndicator()
152   }
153
154   inner class WrapperProgressIndicator : EmptyProgressIndicator() {
155     var myFraction: Long = 0
156     override fun setText(text: String) = doIfImportInProcess {
157       addText(SyncBundle.message("maven.sync.wrapper"), text, true)
158     }
159
160     override fun setFraction(fraction: Double) = doIfImportInProcess {
161       val newFraction = (fraction * 100).toLong()
162       if (myFraction == newFraction) return@doIfImportInProcess
163
164       mySyncView.onEvent(mySyncId,
165                          ProgressBuildEventImpl(SyncBundle.message("maven.sync.wrapper"), SyncBundle.message("maven.sync.wrapper"),
166                                                 System.currentTimeMillis(),
167                                                 SyncBundle.message("maven.sync.wrapper.dowloading"),
168                                                 100,
169                                                 (fraction * 100).toLong(),
170                                                 "%"
171                          ))
172     }
173   }
174
175   @Synchronized
176   fun notifyReadingProblems(file: VirtualFile) = doIfImportInProcess {
177     debugLog("reading problems in $file")
178     hasErrors = true
179     val desc = SyncBundle.message("maven.sync.failure.error.reading.file", file.path)
180     mySyncView.onEvent(mySyncId,
181                        FileMessageEventImpl(mySyncId, MessageEvent.Kind.ERROR, SyncBundle.message("maven.sync.group.error"), desc, desc,
182                                             FilePosition(File(file.path), -1, -1)))
183   }
184
185   @Synchronized
186   @ApiStatus.Internal
187   fun addException(e: Throwable, progressListener: BuildProgressListener) {
188     if(started && !finished){
189       MavenLog.LOG.warn(e)
190       hasErrors = true
191       mySyncView.onEvent(mySyncId,
192                          MessageEventImpl(mySyncId, MessageEvent.Kind.ERROR, "Error", e.localizedMessage, ExceptionUtil.getThrowableText(e)))
193     } else {
194       this.startImport(progressListener)
195       this.addException(e, progressListener)
196       this.finishImport()
197     }
198   }
199
200   fun getListener(type: MavenServerProgressIndicator.ResolveType): ArtifactSyncListener {
201     return when (type) {
202       MavenServerProgressIndicator.ResolveType.PLUGIN -> ArtifactSyncListenerImpl("maven.sync.plugins")
203       MavenServerProgressIndicator.ResolveType.DEPENDENCY -> ArtifactSyncListenerImpl("maven.sync.dependencies")
204     }
205   }
206
207   @Synchronized
208   private fun doFinish() {
209     val tasks = myStartedSet.toList().asReversed()
210     debugLog("Tasks $tasks are not completed! Force complete")
211     tasks.forEach { completeTask(it.first, it.second, DerivedResultImpl()) }
212     mySyncView.onEvent(mySyncId, FinishBuildEventImpl(mySyncId, null, System.currentTimeMillis(), "",
213                                                       if (hasErrors) FailureResultImpl() else DerivedResultImpl()))
214     val generalSettings = MavenWorkspaceSettingsComponent.getInstance(myProject).settings.generalSettings
215     if (hasUnresolved && generalSettings.isWorkOffline) {
216       mySyncView.onEvent(mySyncId, BuildIssueEventImpl(mySyncId, object : BuildIssue {
217         override val title: String = "Dependency Resolution Failed"
218         override val description: String = "<a href=\"${OffMavenOfflineModeQuickFix.ID}\">Switch Off Offline Mode</a>\n"
219         override val quickFixes: List<BuildIssueQuickFix> = listOf(OffMavenOfflineModeQuickFix())
220
221         override fun getNavigatable(project: Project): Navigatable? = null
222       }, MessageEvent.Kind.ERROR))
223     }
224     finished = true
225     started = false
226   }
227
228   @Synchronized
229   private fun showError(keyPrefix: String, dependency: String) = doIfImportInProcess {
230     hasErrors = true
231     hasUnresolved = true
232     val umbrellaString = SyncBundle.message("${keyPrefix}.resolve")
233     val errorString = SyncBundle.message("${keyPrefix}.resolve.error", dependency)
234     startTask(mySyncId, umbrellaString)
235     mySyncView.onEvent(mySyncId, MessageEventImpl(umbrellaString, MessageEvent.Kind.ERROR, "Error", errorString, errorString))
236     addText(mySyncId, errorString, false)
237   }
238
239   @Synchronized
240   private fun startTask(parentId: Any, taskName: String) = doIfImportInProcess {
241     debugLog("Maven sync: start $taskName")
242     if (myStartedSet.add(parentId to taskName)) {
243       mySyncView.onEvent(mySyncId, StartEventImpl(taskName, parentId, System.currentTimeMillis(), taskName))
244     }
245   }
246
247
248   @Synchronized
249   private fun completeTask(parentId: Any, taskName: String, result: EventResult) = doIfImportInProcess {
250     hasErrors = hasErrors || result is FailureResultImpl
251
252     debugLog("Maven sync: complete $taskName with $result")
253     if (myStartedSet.remove(parentId to taskName)) {
254       mySyncView.onEvent(mySyncId, FinishEventImpl(taskName, parentId, System.currentTimeMillis(), taskName, result))
255     }
256   }
257
258
259   private fun debugLog(s: String, exception: Throwable? = null) {
260     MavenLog.LOG.debug(s, exception)
261   }
262
263   @Synchronized
264   private fun completeUmbrellaEvents(keyPrefix: String) = doIfImportInProcess {
265     val taskName = SyncBundle.message("${keyPrefix}.resolve")
266     completeTask(mySyncId, taskName, DerivedResultImpl())
267   }
268
269   @Synchronized
270   private fun downloadEventStarted(keyPrefix: String, dependency: String) = doIfImportInProcess {
271     val downloadString = SyncBundle.message("${keyPrefix}.download")
272     val downloadArtifactString = SyncBundle.message("${keyPrefix}.artifact.download", dependency)
273     startTask(mySyncId, downloadString)
274     startTask(downloadString, downloadArtifactString)
275   }
276
277   @Synchronized
278   private fun downloadEventCompleted(keyPrefix: String, dependency: String) = doIfImportInProcess {
279     val downloadString = SyncBundle.message("${keyPrefix}.download")
280     val downloadArtifactString = SyncBundle.message("${keyPrefix}.artifact.download", dependency)
281     addText(downloadArtifactString, downloadArtifactString, true)
282     completeTask(downloadString, downloadArtifactString, SuccessResultImpl(false))
283   }
284
285
286   @Synchronized
287   private fun downloadEventFailed(keyPrefix: String, dependency: String, error: String, stackTrace: String?) = doIfImportInProcess {
288     val downloadString = SyncBundle.message("${keyPrefix}.download")
289
290     val downloadArtifactString = SyncBundle.message("${keyPrefix}.artifact.download", dependency)
291     if (isJavadocOrSource(dependency)) {
292       addText(downloadArtifactString, "$dependency not found", true)
293       completeTask(downloadString, downloadArtifactString, object : MessageEventResult {
294         override fun getKind(): MessageEvent.Kind {
295           return MessageEvent.Kind.WARNING
296         }
297
298         override fun getDetails(): String? {
299           return SyncBundle.message("maven.sync.failure.dependency.not.found", dependency)
300         }
301       })
302
303     }
304     else {
305       if (stackTrace != null && Registry.`is`("maven.spy.events.debug")) {
306         addText(downloadArtifactString, stackTrace, false)
307       }
308       else {
309         addText(downloadArtifactString, error, true)
310       }
311       completeTask(downloadString, downloadArtifactString, FailureResultImpl(error))
312     }
313   }
314
315   @Synchronized
316   fun showQuickFixBadMaven(message: String, kind: MessageEvent.Kind) {
317     val bundledVersion = MavenServerManager.getInstance().getMavenVersion(MavenServerManager.BUNDLED_MAVEN_3)
318     mySyncView.onEvent(mySyncId, BuildIssueEventImpl(mySyncId, object : BuildIssue {
319       override val title = SyncBundle.message("maven.sync.version.issue.title")
320       override val description: String = "${message}\n" +
321                                          "- <a href=\"${OpenMavenSettingsQuickFix.ID}\">" +
322                                          SyncBundle.message("maven.sync.version.open.settings") + "</a>\n" +
323                                          "- <a href=\"${UseBundledMavenQuickFix.ID}\">" +
324                                          SyncBundle.message("maven.sync.version.use.bundled", bundledVersion) + "</a>\n"
325
326       override val quickFixes: List<BuildIssueQuickFix> = listOf(OpenMavenSettingsQuickFix(), UseBundledMavenQuickFix())
327       override fun getNavigatable(project: Project): Navigatable? = null
328     }, kind))
329   }
330
331   @Synchronized
332   fun showQuickFixJDK(version: String) {
333     mySyncView.onEvent(mySyncId, BuildIssueEventImpl(mySyncId, object : BuildIssue {
334       override val title = SyncBundle.message("maven.sync.quickfixes.maven.jdk.version.title")
335       override val description: String = SyncBundle.message("maven.sync.quickfixes.upgrade.to.jdk7", version) + "\n" +
336                                          "- <a href=\"${OpenMavenSettingsQuickFix.ID}\">" +
337                                          SyncBundle.message("maven.sync.quickfixes.open.settings") +
338                                          "</a>\n"
339       override val quickFixes: List<BuildIssueQuickFix> = listOf(OpenMavenSettingsQuickFix())
340       override fun getNavigatable(project: Project): Navigatable? = null
341     }, MessageEvent.Kind.ERROR))
342   }
343
344   private fun isJavadocOrSource(dependency: String): Boolean {
345     val split = dependency.split(':')
346     if (split.size < 4) {
347       return false
348     }
349     val classifier = split.get(2)
350     return JAVADOC_AND_SOURCE_CLASSIFIERS.contains(classifier)
351   }
352
353   private inline fun doIfImportInProcess(action: () -> Unit) {
354     if (!started || finished) return
355     action.invoke()
356   }
357
358
359   private inner class ArtifactSyncListenerImpl(val keyPrefix: String) : ArtifactSyncListener {
360     override fun downloadStarted(dependency: String) {
361       downloadEventStarted(keyPrefix, dependency)
362     }
363
364     override fun downloadCompleted(dependency: String) {
365       downloadEventCompleted(keyPrefix, dependency)
366     }
367
368     override fun downloadFailed(dependency: String, error: String, stackTrace: String?) {
369       downloadEventFailed(keyPrefix, dependency, error, stackTrace)
370     }
371
372     override fun finish() {
373       completeUmbrellaEvents(keyPrefix)
374     }
375
376     override fun showError(dependency: String) {
377       showError(keyPrefix, dependency)
378     }
379   }
380
381 }
382
383 interface ArtifactSyncListener {
384   fun showError(dependency: String)
385   fun downloadStarted(dependency: String)
386   fun downloadCompleted(dependency: String)
387   fun downloadFailed(dependency: String, error: String, stackTrace: String?)
388   fun finish()
389 }
390
391
392
393