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