annotation processing "proc:only" mode support (IDEA-253719)
[idea/community.git] / java / idea-ui / src / com / intellij / ide / SetupJavaProjectFromSourcesActivity.kt
1 // Copyright 2000-2020 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 com.intellij.ide
3
4 import com.google.common.collect.ArrayListMultimap
5 import com.google.common.collect.Multimap
6 import com.intellij.ide.impl.NewProjectUtil
7 import com.intellij.ide.impl.NewProjectUtil.setCompilerOutputPath
8 import com.intellij.ide.impl.ProjectViewSelectInTarget
9 import com.intellij.ide.projectView.impl.ProjectViewPane
10 import com.intellij.ide.util.DelegatingProgressIndicator
11 import com.intellij.ide.util.importProject.JavaModuleInsight
12 import com.intellij.ide.util.importProject.LibrariesDetectionStep
13 import com.intellij.ide.util.importProject.RootDetectionProcessor
14 import com.intellij.ide.util.projectWizard.WizardContext
15 import com.intellij.ide.util.projectWizard.importSources.impl.ProjectFromSourcesBuilderImpl
16 import com.intellij.notification.*
17 import com.intellij.openapi.application.ApplicationManager
18 import com.intellij.openapi.application.WriteAction
19 import com.intellij.openapi.diagnostic.logger
20 import com.intellij.openapi.fileTypes.FileTypeRegistry
21 import com.intellij.openapi.progress.ProgressIndicator
22 import com.intellij.openapi.progress.ProgressManager
23 import com.intellij.openapi.progress.Task
24 import com.intellij.openapi.project.Project
25 import com.intellij.openapi.projectRoots.JavaSdk
26 import com.intellij.openapi.projectRoots.Sdk
27 import com.intellij.openapi.roots.ui.configuration.ModulesProvider
28 import com.intellij.openapi.roots.ui.configuration.ProjectSettingsService
29 import com.intellij.openapi.roots.ui.configuration.SdkLookup
30 import com.intellij.openapi.roots.ui.configuration.SdkLookupDecision
31 import com.intellij.openapi.startup.StartupActivity
32 import com.intellij.openapi.util.NlsSafe
33 import com.intellij.openapi.vfs.*
34 import com.intellij.platform.PlatformProjectOpenProcessor
35 import com.intellij.projectImport.ProjectOpenProcessor
36 import com.intellij.util.ThrowableRunnable
37 import java.io.File
38 import java.util.concurrent.CompletableFuture
39 import javax.swing.event.HyperlinkEvent
40
41 private val NOTIFICATION_GROUP = NotificationGroup("Build Script Found", NotificationDisplayType.STICKY_BALLOON, true)
42
43 private const val SCAN_DEPTH_LIMIT = 5
44 private const val MAX_ROOTS_IN_TRIVIAL_PROJECT_STRUCTURE = 3
45 private val LOG = logger<SetupJavaProjectFromSourcesActivity>()
46
47 internal class SetupJavaProjectFromSourcesActivity : StartupActivity {
48   override fun runActivity(project: Project) {
49     if (ApplicationManager.getApplication().isHeadlessEnvironment) {
50       return
51     }
52     if (project.hasBeenOpenedBySpecificProcessor()) {
53       return
54     }
55
56     // todo get current project structure, and later setup from sources only if it wasn't manually changed by the user
57
58     val title = JavaUiBundle.message("task.searching.for.project.sources")
59     ProgressManager.getInstance().run(object: Task.Backgroundable(project, title, true) {
60       override fun run(indicator: ProgressIndicator) {
61         val projectDir = project.baseDir
62         val importers = searchImporters(projectDir)
63         if (!importers.isEmpty) {
64           showNotificationToImport(project, projectDir, importers)
65         }
66         else {
67           setupFromSources(project, projectDir, indicator)
68         }
69       }
70     })
71   }
72
73   private fun Project.hasBeenOpenedBySpecificProcessor(): Boolean {
74     return getUserData(PlatformProjectOpenProcessor.PROJECT_OPENED_BY_PLATFORM_PROCESSOR) != true
75   }
76
77   private fun searchImporters(projectDirectory: VirtualFile): ArrayListMultimap<ProjectOpenProcessor, VirtualFile> {
78     val providersAndFiles = ArrayListMultimap.create<ProjectOpenProcessor, VirtualFile>()
79     VfsUtil.visitChildrenRecursively(projectDirectory, object : VirtualFileVisitor<Void>(NO_FOLLOW_SYMLINKS, limit(SCAN_DEPTH_LIMIT)) {
80       override fun visitFileEx(file: VirtualFile): Result {
81         if (file.isDirectory && FileTypeRegistry.getInstance().isFileIgnored(file)) {
82           return SKIP_CHILDREN
83         }
84
85         val providers = ProjectOpenProcessor.EXTENSION_POINT_NAME.extensionList.filter { provider ->
86           provider.canOpenProject(file) &&
87           provider !is PlatformProjectOpenProcessor
88         }
89
90         for (provider in providers) {
91           val files = providersAndFiles.get(provider)
92           if (files.isEmpty()) {
93             files.add(file)
94           }
95           else if (!VfsUtilCore.isAncestor(files.last(), file, true)) { // add only top-level file/folders for each of providers
96             files.add(file)
97           }
98         }
99         return CONTINUE
100       }
101     })
102     return providersAndFiles
103   }
104
105   private fun showNotificationToImport(project: Project,
106                                        projectDirectory: VirtualFile,
107                                        providersAndFiles: ArrayListMultimap<ProjectOpenProcessor, VirtualFile>) {
108     val showFileInProjectViewListener = object : NotificationListener.Adapter() {
109       override fun hyperlinkActivated(notification: Notification, e: HyperlinkEvent) {
110         val file = LocalFileSystem.getInstance().findFileByPath(e.description)
111         ProjectViewSelectInTarget.select(project, file, ProjectViewPane.ID, null, file, true)
112       }
113     }
114
115     val title: String
116     val content: String
117     if (providersAndFiles.keySet().size == 1) {
118       val processor = providersAndFiles.keySet().single()
119       val files = providersAndFiles[processor]
120       title = JavaUiBundle.message("build.script.found.notification", processor.name, files.size)
121       content = filesToLinks(files, projectDirectory)
122     }
123     else {
124       title = JavaUiBundle.message("build.scripts.from.multiple.providers.found.notification")
125       content = formatContent(providersAndFiles, projectDirectory)
126     }
127
128     val notification = NOTIFICATION_GROUP.createNotification(title, content, NotificationType.INFORMATION, showFileInProjectViewListener)
129
130     if (providersAndFiles.keySet().all { it.canImportProjectAfterwards() }) {
131       val actionName = if (providersAndFiles.keySet().size > 1) {
132         JavaUiBundle.message("build.script.found.notification.import.all")
133       }
134       else {
135         JavaUiBundle.message("build.script.found.notification.import")
136       }
137       notification.addAction(NotificationAction.createSimpleExpiring(actionName) {
138         for ((provider, files) in providersAndFiles.asMap()) {
139           for (file in files) {
140             provider.importProjectAfterwards(project, file)
141           }
142         }
143       })
144     }
145
146     notification.notify(project)
147   }
148
149   @NlsSafe
150   private fun formatContent(providersAndFiles: Multimap<ProjectOpenProcessor, VirtualFile>,
151                             projectDirectory: VirtualFile): String {
152     return providersAndFiles.asMap().entries.joinToString("<br/>") { (provider, files) ->
153       provider.name + ": " + filesToLinks(files, projectDirectory)
154     }
155   }
156
157   @NlsSafe
158   private fun filesToLinks(files: MutableCollection<VirtualFile>, projectDirectory: VirtualFile) =
159     files.joinToString { file ->
160       "<a href='${file.path}'>${VfsUtil.getRelativePath(file, projectDirectory)}</a>"
161   }
162
163   private fun setupFromSources(project: Project,
164                                projectDir: VirtualFile,
165                                indicator: ProgressIndicator) {
166     val builder = ProjectFromSourcesBuilderImpl(WizardContext(project, project), ModulesProvider.EMPTY_MODULES_PROVIDER)
167     val projectPath = projectDir.path
168     builder.baseProjectPath = projectPath
169     val roots = RootDetectionProcessor.detectRoots(File(projectPath))
170     val rootsMap = RootDetectionProcessor.createRootsMap(roots)
171     builder.setupProjectStructure(rootsMap)
172     for (detector in rootsMap.keySet()) {
173       val descriptor = builder.getProjectDescriptor(detector)
174
175       val moduleInsight = JavaModuleInsight(DelegatingProgressIndicator(), builder.existingModuleNames, builder.existingProjectLibraryNames)
176       descriptor.libraries = LibrariesDetectionStep.calculate(moduleInsight, builder)
177
178       moduleInsight.scanModules()
179       descriptor.modules = moduleInsight.suggestedModules
180     }
181
182     ApplicationManager.getApplication().invokeAndWait {
183       builder.commit(project)
184
185       val compileOutput = if (projectPath.endsWith('/')) "${projectPath}out" else "$projectPath/out"
186       setCompilerOutputPath(project, compileOutput)
187     }
188
189     findAndSetupJdk(project, indicator)
190
191     if (roots.size > MAX_ROOTS_IN_TRIVIAL_PROJECT_STRUCTURE) {
192       notifyAboutAutomaticProjectStructure(project)
193     }
194   }
195
196   private fun findAndSetupJdk(project: Project, indicator: ProgressIndicator) {
197     val future = CompletableFuture<Sdk>()
198     SdkLookup.newLookupBuilder()
199       .withProgressIndicator(indicator)
200       .withSdkType(JavaSdk.getInstance())
201       .withVersionFilter { true }
202       .withProject(project)
203       .onDownloadableSdkSuggested { SdkLookupDecision.STOP }
204       .onSdkResolved {
205         future.complete(it)
206       }
207       .executeLookup()
208
209     try {
210       val sdk = future.get()
211       if (sdk != null) {
212         WriteAction.runAndWait(
213           ThrowableRunnable<Throwable> {
214             NewProjectUtil.applyJdkToProject(project, sdk)
215           }
216         )
217       }
218     }
219     catch (t: Throwable) {
220       LOG.warn("Couldn't lookup for a JDK", t)
221     }
222   }
223
224   private fun notifyAboutAutomaticProjectStructure(project: Project) {
225     val message = JavaUiBundle.message("project.structure.automatically.detected.notification")
226     val notification = NOTIFICATION_GROUP.createNotification("", message, NotificationType.INFORMATION, null)
227     notification.addAction(NotificationAction.createSimpleExpiring(
228       JavaUiBundle.message("project.structure.automatically.detected.notification.gotit.action")) {})
229     notification.addAction(NotificationAction.createSimpleExpiring(
230       JavaUiBundle.message("project.structure.automatically.detected.notification.configure.action")) {
231       ProjectSettingsService.getInstance(project).openProjectSettings()
232     })
233     notification.notify(project)
234   }
235 }