mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
IJPL-207056: Move WebStarterModuleBuilder to intellij.java.ui
GitOrigin-RevId: b66c6a62ffbb751e0e4af0592d8e2ea7b95da7bd
This commit is contained in:
committed by
intellij-monorepo-bot
parent
c98d45b58b
commit
64497ad7a5
@@ -62,6 +62,8 @@ jvm_library(
|
||||
"@lib//:slf4j-api",
|
||||
"//xml/xml-psi-api:psi",
|
||||
"@lib//:asm",
|
||||
"@lib//:jackson",
|
||||
"@lib//:jackson-databind",
|
||||
],
|
||||
runtime_deps = [":ui_resources"]
|
||||
)
|
||||
|
||||
@@ -58,5 +58,7 @@
|
||||
<orderEntry type="library" name="slf4j-api" level="project" />
|
||||
<orderEntry type="module" module-name="intellij.xml.psi" />
|
||||
<orderEntry type="library" name="ASM" level="project" />
|
||||
<orderEntry type="library" name="jackson" level="project" />
|
||||
<orderEntry type="library" name="jackson-databind" level="project" />
|
||||
</component>
|
||||
</module>
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.ide.starters.remote
|
||||
|
||||
import com.intellij.ide.starters.shared.CommonStarterContext
|
||||
import com.intellij.ide.starters.shared.DEFAULT_PACKAGE_NAME
|
||||
import com.intellij.ide.starters.shared.StarterAppPackaging
|
||||
import com.intellij.ide.starters.shared.StarterLanguageLevel
|
||||
|
||||
class WebStarterContext : CommonStarterContext() {
|
||||
lateinit var serverUrl: String
|
||||
lateinit var serverOptions: WebStarterServerOptions
|
||||
|
||||
var frameworkVersion: WebStarterFrameworkVersion? = null
|
||||
var packageName: String = DEFAULT_PACKAGE_NAME
|
||||
var languageLevel: StarterLanguageLevel? = null
|
||||
var packaging: StarterAppPackaging? = null
|
||||
|
||||
val dependencies: MutableSet<WebStarterDependency> = HashSet()
|
||||
|
||||
var result: DownloadResult? = null
|
||||
}
|
||||
@@ -0,0 +1,439 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.ide.starters.remote
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.intellij.codeInsight.actions.ReformatCodeProcessor
|
||||
import com.intellij.ide.IdeBundle
|
||||
import com.intellij.ide.plugins.PluginManagerCore
|
||||
import com.intellij.ide.projectWizard.ProjectSettingsStep
|
||||
import com.intellij.ide.starters.JavaStartersBundle
|
||||
import com.intellij.ide.starters.local.StarterModuleBuilder
|
||||
import com.intellij.ide.starters.local.StarterModuleBuilder.Companion.importModule
|
||||
import com.intellij.ide.starters.local.StarterModuleBuilder.Companion.openSampleFiles
|
||||
import com.intellij.ide.starters.local.StarterModuleBuilder.Companion.preprocessModuleCreated
|
||||
import com.intellij.ide.starters.local.StarterModuleBuilder.Companion.preprocessModuleOpened
|
||||
import com.intellij.ide.starters.remote.wizard.WebStarterInitialStep
|
||||
import com.intellij.ide.starters.remote.wizard.WebStarterLibrariesStep
|
||||
import com.intellij.ide.starters.shared.*
|
||||
import com.intellij.ide.util.projectWizard.ModuleBuilder
|
||||
import com.intellij.ide.util.projectWizard.ModuleWizardStep
|
||||
import com.intellij.ide.util.projectWizard.SettingsStep
|
||||
import com.intellij.ide.util.projectWizard.WizardContext
|
||||
import com.intellij.notification.NotificationAction
|
||||
import com.intellij.notification.NotificationType
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.GitRepositoryInitializer
|
||||
import com.intellij.openapi.application.ApplicationInfo
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.application.ApplicationNamesInfo
|
||||
import com.intellij.openapi.application.ModalityState
|
||||
import com.intellij.openapi.diagnostic.thisLogger
|
||||
import com.intellij.openapi.extensions.PluginId
|
||||
import com.intellij.openapi.externalSystem.util.ExternalSystemUtil
|
||||
import com.intellij.openapi.module.JavaModuleType
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.module.ModuleType
|
||||
import com.intellij.openapi.options.ConfigurationException
|
||||
import com.intellij.openapi.progress.ProgressIndicator
|
||||
import com.intellij.openapi.progress.runBackgroundableTask
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.projectRoots.JavaSdk
|
||||
import com.intellij.openapi.projectRoots.JavaSdkType
|
||||
import com.intellij.openapi.projectRoots.JavaSdkVersion
|
||||
import com.intellij.openapi.projectRoots.SdkTypeId
|
||||
import com.intellij.openapi.roots.LanguageLevelModuleExtension
|
||||
import com.intellij.openapi.roots.ModifiableRootModel
|
||||
import com.intellij.openapi.roots.ui.configuration.ModulesProvider
|
||||
import com.intellij.openapi.roots.ui.configuration.setupNewModuleJdk
|
||||
import com.intellij.openapi.startup.StartupManager
|
||||
import com.intellij.openapi.ui.Messages
|
||||
import com.intellij.openapi.updateSettings.impl.pluginsAdvertisement.getPluginSuggestionNotificationGroup
|
||||
import com.intellij.openapi.updateSettings.impl.pluginsAdvertisement.installAndEnable
|
||||
import com.intellij.openapi.util.NlsSafe
|
||||
import com.intellij.openapi.util.io.FileUtil
|
||||
import com.intellij.openapi.util.text.StringUtil
|
||||
import com.intellij.openapi.vfs.LocalFileSystem
|
||||
import com.intellij.util.Url
|
||||
import com.intellij.util.concurrency.EdtExecutorService
|
||||
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
|
||||
import com.intellij.util.io.HttpRequests
|
||||
import com.intellij.util.io.HttpRequests.RequestProcessor
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.net.URLConnection
|
||||
import java.net.UnknownHostException
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.swing.Icon
|
||||
|
||||
abstract class WebStarterModuleBuilder : ModuleBuilder() {
|
||||
protected val starterContext: WebStarterContext = WebStarterContext()
|
||||
protected val starterSettings: StarterWizardSettings by lazy { createSettings() }
|
||||
|
||||
protected val objectMapper: ObjectMapper by lazy { ObjectMapper() }
|
||||
|
||||
override fun getModuleType(): ModuleType<*> = JavaModuleType.getModuleType()
|
||||
override fun getWeight(): Int = JVM_WEIGHT
|
||||
open fun getHelpId(): String? = null
|
||||
|
||||
// Required settings
|
||||
|
||||
abstract override fun getBuilderId(): String
|
||||
abstract override fun getNodeIcon(): Icon?
|
||||
abstract override fun getPresentableName(): String
|
||||
abstract override fun getDescription(): String
|
||||
|
||||
abstract fun getDefaultServerUrl(): String
|
||||
|
||||
protected abstract fun getLanguages(): List<StarterLanguage>
|
||||
protected abstract fun getProjectTypes(): List<StarterProjectType>
|
||||
|
||||
// Optional settings
|
||||
|
||||
protected open fun getDefaultVersion(): String = DEFAULT_MODULE_VERSION
|
||||
protected open fun isPackageNameEditable(): Boolean = false
|
||||
protected open fun isExampleCodeProvided(): Boolean = false
|
||||
protected open fun getTestFrameworks(): List<StarterTestRunner> = emptyList()
|
||||
protected open fun getLanguageLevels(): List<StarterLanguageLevel> = emptyList()
|
||||
protected open fun getDefaultLanguageLevel(): StarterLanguageLevel? = null
|
||||
protected open fun getApplicationTypes(): List<StarterAppType> = emptyList()
|
||||
protected open fun getPackagingTypes(): List<StarterAppPackaging> = emptyList()
|
||||
|
||||
protected open fun getFilePathsToOpen(): List<String> = emptyList()
|
||||
|
||||
protected open fun isShowProjectTypes(): Boolean = true
|
||||
|
||||
override fun isSuitableSdkType(sdkType: SdkTypeId): Boolean {
|
||||
return sdkType is JavaSdkType && !sdkType.isDependent
|
||||
}
|
||||
|
||||
private fun createSettings(): StarterWizardSettings {
|
||||
return StarterWizardSettings(
|
||||
getProjectTypes(),
|
||||
getLanguages(),
|
||||
isExampleCodeProvided(),
|
||||
isPackageNameEditable(),
|
||||
getLanguageLevels(),
|
||||
getDefaultLanguageLevel(),
|
||||
getPackagingTypes(),
|
||||
getApplicationTypes(),
|
||||
getTestFrameworks(),
|
||||
getCustomizedMessages(),
|
||||
isShowProjectTypes()
|
||||
)
|
||||
}
|
||||
|
||||
override fun getCustomOptionsStep(context: WizardContext, parentDisposable: Disposable): ModuleWizardStep? {
|
||||
starterContext.serverUrl = getDefaultServerUrl()
|
||||
starterContext.version = getDefaultVersion()
|
||||
starterContext.language = starterSettings.languages.first()
|
||||
starterContext.projectType = starterSettings.projectTypes.firstOrNull()
|
||||
starterContext.isCreatingNewProject = context.isCreatingNewProject
|
||||
|
||||
starterContext.applicationType = starterSettings.applicationTypes.firstOrNull()
|
||||
starterContext.languageLevel = starterSettings.defaultLanguageLevel ?: starterSettings.languageLevels.firstOrNull()
|
||||
starterContext.packaging = starterSettings.packagingTypes.firstOrNull()
|
||||
starterContext.testFramework = starterSettings.testFrameworks.firstOrNull()
|
||||
|
||||
return createOptionsStep(WebStarterContextProvider(this, context, starterContext, starterSettings, parentDisposable))
|
||||
}
|
||||
|
||||
override fun createWizardSteps(context: WizardContext, modulesProvider: ModulesProvider): Array<ModuleWizardStep> {
|
||||
return arrayOf(createLibrariesStep(WebStarterContextProvider(this, context, starterContext, starterSettings, context.disposable)))
|
||||
}
|
||||
|
||||
override fun getIgnoredSteps(): List<Class<out ModuleWizardStep>> {
|
||||
return listOf(ProjectSettingsStep::class.java)
|
||||
}
|
||||
|
||||
override fun modifyProjectTypeStep(settingsStep: SettingsStep): ModuleWizardStep? {
|
||||
// do not add standard SDK selector at the top
|
||||
return null
|
||||
}
|
||||
|
||||
protected open fun createOptionsStep(contextProvider: WebStarterContextProvider): WebStarterInitialStep {
|
||||
return WebStarterInitialStep(contextProvider)
|
||||
}
|
||||
|
||||
protected open fun createLibrariesStep(contextProvider: WebStarterContextProvider): WebStarterLibrariesStep {
|
||||
return WebStarterLibrariesStep(contextProvider)
|
||||
}
|
||||
|
||||
private fun getUserAgentInternal(): String? = getUserAgent()
|
||||
|
||||
protected open fun getUserAgent(): String? {
|
||||
return ApplicationNamesInfo.getInstance().fullProductName + "/" + ApplicationInfo.getInstance().fullVersion
|
||||
}
|
||||
|
||||
protected open fun getCustomizedMessages(): CustomizedMessages? = null
|
||||
|
||||
@RequiresBackgroundThread
|
||||
fun getServerOptions(serverUrl: String): WebStarterServerOptions = loadServerOptions(serverUrl)
|
||||
|
||||
@RequiresBackgroundThread
|
||||
protected abstract fun loadServerOptions(serverUrl: String): WebStarterServerOptions
|
||||
|
||||
internal fun getDependencyStateInternal(frameworkVersion: WebStarterFrameworkVersion, dependency: WebStarterDependency): DependencyState {
|
||||
return getDependencyState(frameworkVersion, dependency)
|
||||
}
|
||||
|
||||
internal fun isVersionAvailableInternal(frameworkVersion: WebStarterFrameworkVersion): Boolean {
|
||||
return isVersionAvailable(frameworkVersion)
|
||||
}
|
||||
|
||||
protected open fun getDependencyState(frameworkVersion: WebStarterFrameworkVersion, dependency: WebStarterDependency): DependencyState {
|
||||
return DependencyAvailable
|
||||
}
|
||||
|
||||
protected open fun isVersionAvailable(frameworkVersion: WebStarterFrameworkVersion): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
private fun getGeneratorUrlInternal(serverUrl: String, starterContext: WebStarterContext): Url {
|
||||
return composeGeneratorUrl(serverUrl, starterContext)
|
||||
}
|
||||
|
||||
protected abstract fun composeGeneratorUrl(serverUrl: String, starterContext: WebStarterContext): Url
|
||||
|
||||
protected abstract fun extractGeneratorResult(tempZipFile: File, contentEntryDir: File)
|
||||
|
||||
protected open fun getPluginRecommendations(): List<PluginRecommendation> = emptyList()
|
||||
|
||||
protected open fun isReformatAfterCreation(project: Project): Boolean = true
|
||||
|
||||
override fun createProject(name: String, path: String): Project? {
|
||||
val project = super.createProject(name, path)
|
||||
project?.let { StarterModuleBuilder.setupProject(it) }
|
||||
return project
|
||||
}
|
||||
|
||||
@Throws(ConfigurationException::class)
|
||||
override fun setupModule(module: Module) {
|
||||
super.setupModule(module)
|
||||
|
||||
val isMavenProject = starterContext.projectType?.id?.contains("Maven", ignoreCase = true) ?: false
|
||||
ExternalSystemUtil.configureNewModule(module, starterContext.isCreatingNewProject, isMavenProject)
|
||||
|
||||
val project = module.project
|
||||
|
||||
try {
|
||||
extractTemplate() // should be quite fast, only extracts single small ZIP file
|
||||
}
|
||||
catch (e: Exception) {
|
||||
thisLogger().info(e)
|
||||
|
||||
StartupManager.getInstance(project).runAfterOpened {
|
||||
EdtExecutorService.getScheduledExecutorInstance().schedule(
|
||||
{
|
||||
var message = JavaStartersBundle.message("error.text.with.error.content", e.message)
|
||||
message = StringUtil.shortenTextWithEllipsis(message, 1024, 0) // exactly 1024 because why not
|
||||
Messages.showErrorDialog(message, presentableName)
|
||||
},
|
||||
3, TimeUnit.SECONDS)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
preprocessModuleCreated(module, this, starterContext.frameworkVersion?.id)
|
||||
|
||||
StartupManager.getInstance(project).runAfterOpened {
|
||||
ApplicationManager.getApplication().invokeLater({ runImport(module) },
|
||||
ModalityState.nonModal(), module.disposed)
|
||||
}
|
||||
}
|
||||
|
||||
override fun setupRootModel(modifiableRootModel: ModifiableRootModel) {
|
||||
val sdk = setupNewModuleJdk(modifiableRootModel, moduleJdk, starterContext.isCreatingNewProject)
|
||||
val moduleExt = modifiableRootModel.getModuleExtension(LanguageLevelModuleExtension::class.java)
|
||||
if (moduleExt != null && sdk != null) {
|
||||
val languageLevel = starterContext.languageLevel
|
||||
if (languageLevel != null) {
|
||||
val selectedVersion = JavaSdkVersion.fromVersionString(languageLevel.id)
|
||||
val sdkVersion = JavaSdk.getInstance().getVersion(sdk)
|
||||
if (selectedVersion != null && sdkVersion != null && sdkVersion.isAtLeast(selectedVersion)) {
|
||||
moduleExt.languageLevel = selectedVersion.maxLanguageLevel
|
||||
}
|
||||
}
|
||||
}
|
||||
doAddContentEntry(modifiableRootModel)
|
||||
}
|
||||
|
||||
private fun runImport(module: Module) {
|
||||
LocalFileSystem.getInstance().refresh(false) // to avoid IDEA-232806
|
||||
|
||||
preprocessModuleOpened(module, this, starterContext.frameworkVersion?.id)
|
||||
|
||||
if (isReformatAfterCreation(module.project)) {
|
||||
ReformatCodeProcessor(module.project, module, false).run()
|
||||
}
|
||||
|
||||
openSampleFiles(module, getFilePathsToOpen())
|
||||
|
||||
if (starterContext.gitIntegration && starterContext.isCreatingNewProject) {
|
||||
val moduleContentRoot = LocalFileSystem.getInstance().refreshAndFindFileByPath(contentEntryPath!!.replace("\\", "/"))
|
||||
?: throw IllegalStateException("Module root not found")
|
||||
|
||||
runBackgroundableTask(IdeBundle.message("progress.title.creating.git.repository"), module.project) {
|
||||
GitRepositoryInitializer.getInstance()?.initRepository(module.project, moduleContentRoot, true)
|
||||
}
|
||||
}
|
||||
|
||||
importModule(module)
|
||||
|
||||
verifyIdePlugins(module.project)
|
||||
}
|
||||
|
||||
private fun extractTemplate() {
|
||||
val downloadResult = starterContext.result!!
|
||||
val tempFile = downloadResult.tempFile
|
||||
|
||||
val path: String = contentEntryPath!!
|
||||
val contentEntryDir = File(path)
|
||||
|
||||
if (downloadResult.isZip) {
|
||||
extractGeneratorResult(tempFile, contentEntryDir)
|
||||
fixExecutableFlag(contentEntryDir, "gradlew")
|
||||
fixExecutableFlag(contentEntryDir, "mvnw")
|
||||
}
|
||||
else {
|
||||
FileUtil.copy(tempFile, File(contentEntryDir, downloadResult.filename))
|
||||
}
|
||||
}
|
||||
|
||||
private fun verifyIdePlugins(project: Project) {
|
||||
val selectedDependenciesIds = starterContext.dependencies.map { it.id }.toSet()
|
||||
|
||||
val requiredPluginIds: MutableSet<PluginId> = HashSet()
|
||||
for (pluginRecommendation in getPluginRecommendations()) {
|
||||
for (dependencyId in pluginRecommendation.dependencyIds) {
|
||||
if (selectedDependenciesIds.contains(dependencyId)) {
|
||||
requiredPluginIds.add(PluginId.getId(pluginRecommendation.pluginId))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val toInstallOrEnable: MutableSet<PluginId> = HashSet()
|
||||
for (pluginId in requiredPluginIds) {
|
||||
val ideaPluginDescriptor = PluginManagerCore.getPlugin(pluginId)
|
||||
if (ideaPluginDescriptor == null || !ideaPluginDescriptor.isEnabled) {
|
||||
toInstallOrEnable.add(pluginId)
|
||||
}
|
||||
}
|
||||
|
||||
if (toInstallOrEnable.isEmpty()) return
|
||||
|
||||
getPluginSuggestionNotificationGroup()
|
||||
.createNotification(IdeBundle.message("plugins.advertiser.plugins.suggestions.title"),
|
||||
IdeBundle.message("plugins.advertiser.plugins.suggestions.text"), NotificationType.INFORMATION)
|
||||
.addAction(NotificationAction.create(IdeBundle.message("plugins.advertiser.action.enable.plugins")) { _, notification ->
|
||||
installAndEnable(project, toInstallOrEnable) { notification.expire() }
|
||||
})
|
||||
.notify(project)
|
||||
}
|
||||
|
||||
private fun fixExecutableFlag(containingDir: File, relativePath: String) {
|
||||
val toFix = File(containingDir, relativePath)
|
||||
if (toFix.exists()) {
|
||||
toFix.setExecutable(true, false)
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresBackgroundThread
|
||||
protected fun loadJsonData(url: String, accept: String? = null): JsonNode {
|
||||
return HttpRequests.request(url)
|
||||
.userAgent(getUserAgent())
|
||||
.accept(accept)
|
||||
.connectTimeout(10000)
|
||||
.connect(RequestProcessor { request ->
|
||||
val reader = try {
|
||||
request.reader
|
||||
}
|
||||
catch (e: IOException) {
|
||||
thisLogger().info("IOException loading JSON response from " + request.url, e)
|
||||
throw IOException(HttpRequests.createErrorMessage(e, request, false), e)
|
||||
}
|
||||
|
||||
val jsonRootElement = try {
|
||||
objectMapper.readTree(reader)
|
||||
}
|
||||
catch (e: Throwable) {
|
||||
thisLogger().info("Unable to read JSON response from " + request.url, e)
|
||||
throw IOException("Error parsing JSON response", e)
|
||||
}
|
||||
jsonRootElement ?: throw IOException("Error parsing JSON response: empty document")
|
||||
})
|
||||
}
|
||||
|
||||
@RequiresBackgroundThread
|
||||
internal fun downloadResultInternal(progressIndicator: ProgressIndicator): DownloadResult {
|
||||
val tempFile = FileUtil.createTempFile(builderId, ".tmp", true)
|
||||
return downloadResult(progressIndicator, tempFile)
|
||||
}
|
||||
|
||||
@RequiresBackgroundThread
|
||||
protected open fun downloadResult(progressIndicator: ProgressIndicator, tempFile: File): DownloadResult {
|
||||
val url = getGeneratorUrlInternal(starterContext.serverUrl, starterContext).toExternalForm()
|
||||
thisLogger().info("Loading project from ${url}")
|
||||
|
||||
return HttpRequests
|
||||
.request(url)
|
||||
.userAgent(getUserAgentInternal())
|
||||
.connectTimeout(10000)
|
||||
.isReadResponseOnError(true)
|
||||
.connect(RequestProcessor { request ->
|
||||
handleDownloadResponse(request, tempFile, progressIndicator)
|
||||
})
|
||||
}
|
||||
|
||||
protected fun handleDownloadResponse(request: HttpRequests.Request,
|
||||
tempFile: File,
|
||||
progressIndicator: ProgressIndicator): DownloadResult {
|
||||
val connection: URLConnection = try {
|
||||
request.connection
|
||||
}
|
||||
catch (e: IOException) {
|
||||
thisLogger().warn("Can't download project. Message (with headers info): "
|
||||
+ HttpRequests.createErrorMessage(e, request, true))
|
||||
throw IOException(HttpRequests.createErrorMessage(e, request, false), e)
|
||||
}
|
||||
catch (he: UnknownHostException) {
|
||||
thisLogger().warn("Can't download project: " + he.message)
|
||||
throw IOException(HttpRequests.createErrorMessage(he, request, false), he)
|
||||
}
|
||||
|
||||
val contentType = connection.contentType
|
||||
val contentDisposition = connection.getHeaderField("Content-Disposition")
|
||||
val filename = getFilename(contentDisposition)
|
||||
val isZip = StringUtil.isNotEmpty(contentType) && contentType.startsWith("application/zip")
|
||||
|| filename.endsWith(".zip")
|
||||
// Micronaut has broken content-type (it's "text") but zip-file as attachment
|
||||
// (https://github.com/micronaut-projects/micronaut-starter/issues/268)
|
||||
|
||||
request.saveToFile(tempFile, progressIndicator)
|
||||
|
||||
return DownloadResult(isZip, tempFile, filename)
|
||||
}
|
||||
|
||||
@NlsSafe
|
||||
private fun getFilename(contentDisposition: String?): String {
|
||||
val filenameField = "filename="
|
||||
if (StringUtil.isEmpty(contentDisposition)) return "unknown"
|
||||
|
||||
val startIdx = contentDisposition!!.indexOf(filenameField)
|
||||
val endIdx = contentDisposition.indexOf(';', startIdx)
|
||||
var fileName = contentDisposition.substring(startIdx + filenameField.length, if (endIdx > 0) endIdx else contentDisposition.length)
|
||||
if (StringUtil.startsWithChar(fileName, '\"') && StringUtil.endsWithChar(fileName, '\"')) {
|
||||
fileName = fileName.substring(1, fileName.length - 1)
|
||||
}
|
||||
return fileName
|
||||
}
|
||||
|
||||
fun JsonNode.getNullable(field: String): JsonNode? {
|
||||
val element = this.get(field) ?: return null
|
||||
if (element.isNull) {
|
||||
return null
|
||||
}
|
||||
return element
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
@file:JvmName("WebStarterSettings")
|
||||
|
||||
package com.intellij.ide.starters.remote
|
||||
|
||||
import com.intellij.ide.starters.shared.*
|
||||
import com.intellij.ide.util.projectWizard.WizardContext
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.util.Key
|
||||
import com.intellij.openapi.util.NlsSafe
|
||||
import com.intellij.openapi.util.UserDataHolderBase
|
||||
import com.intellij.util.io.Decompressor
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.nio.file.Paths
|
||||
import java.util.zip.ZipFile
|
||||
|
||||
open class WebStarterServerOptions(
|
||||
val frameworkVersions: List<WebStarterFrameworkVersion>,
|
||||
val dependencyCategories: List<WebStarterDependencyCategory>,
|
||||
) : UserDataHolderBase() {
|
||||
|
||||
fun <T> extractOption(key: Key<T>, handler: (T) -> Unit) {
|
||||
val value = getUserData(key)
|
||||
if (value != null) {
|
||||
handler.invoke(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val SERVER_NAME_KEY: Key<String> = Key.create("name")
|
||||
val SERVER_GROUP_KEY: Key<String> = Key.create("group")
|
||||
val SERVER_ARTIFACT_KEY: Key<String> = Key.create("artifact")
|
||||
val SERVER_VERSION_KEY: Key<String> = Key.create("version")
|
||||
val SERVER_PACKAGE_NAME_KEY: Key<String> = Key.create("packageName")
|
||||
val SERVER_LANGUAGES: Key<List<StarterLanguage>> = Key.create("languages")
|
||||
val SERVER_LANGUAGE_LEVEL_KEY: Key<StarterLanguageLevel> = Key.create("languageLevel")
|
||||
val SERVER_LANGUAGE_LEVELS_KEY: Key<List<StarterLanguageLevel>> = Key.create("languageLevels")
|
||||
val SERVER_PROJECT_TYPES: Key<List<StarterProjectType>> = Key.create("projectTypes")
|
||||
val SERVER_APPLICATION_TYPES: Key<List<StarterAppType>> = Key.create("appTypes")
|
||||
val SERVER_PACKAGING_TYPES: Key<List<StarterAppPackaging>> = Key.create("packagingTypes")
|
||||
|
||||
open class WebStarterFrameworkVersion(
|
||||
val id: String,
|
||||
@NlsSafe val title: String,
|
||||
val isDefault: Boolean
|
||||
) {
|
||||
override fun toString(): String {
|
||||
return "WebStarterFrameworkVersion(version='$id', isDefault=$isDefault)"
|
||||
}
|
||||
}
|
||||
|
||||
open class WebStarterDependencyCategory(
|
||||
@NlsSafe
|
||||
val title: String,
|
||||
val dependencies: List<WebStarterDependency>
|
||||
) {
|
||||
open fun isAvailable(starterContext: WebStarterContext): Boolean = true
|
||||
|
||||
override fun toString(): String {
|
||||
return "WebStarterDependencyCategory(title='$title')"
|
||||
}
|
||||
}
|
||||
|
||||
open class WebStarterDependency(
|
||||
val id: String,
|
||||
@NlsSafe
|
||||
override val title: String,
|
||||
override val description: String? = null,
|
||||
override val links: List<LibraryLink> = emptyList(),
|
||||
override val isDefault: Boolean = false,
|
||||
override val isRequired: Boolean = false
|
||||
) : LibraryInfo {
|
||||
override fun toString(): String {
|
||||
return "WebStarterDependency(id='$id', title='$title')"
|
||||
}
|
||||
}
|
||||
|
||||
class WebStarterContextProvider(
|
||||
val moduleBuilder: WebStarterModuleBuilder,
|
||||
val wizardContext: WizardContext,
|
||||
val starterContext: WebStarterContext,
|
||||
val settings: StarterWizardSettings,
|
||||
val parentDisposable: Disposable
|
||||
)
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun unzipSubfolder(tempZipFile: File, contentEntryDir: File) {
|
||||
var rootFolderName: String
|
||||
|
||||
ZipFile(tempZipFile).use { jar ->
|
||||
val entries = jar.entries()
|
||||
if (!entries.hasMoreElements()) {
|
||||
throw UnexpectedArchiveStructureException("The archive is empty")
|
||||
}
|
||||
val rootFolders = HashSet<String>()
|
||||
while (entries.hasMoreElements()) {
|
||||
val entry = entries.nextElement()
|
||||
val path = Paths.get(entry.name)
|
||||
if (path.nameCount > 0) {
|
||||
rootFolders.add(path.normalize().getName(0).toString())
|
||||
}
|
||||
}
|
||||
if (rootFolders.size != 1) {
|
||||
throw UnexpectedArchiveStructureException(
|
||||
"The archive should have 1 subdirectory, but has: " + rootFolders.joinToString(","))
|
||||
}
|
||||
rootFolderName = rootFolders.iterator().next()
|
||||
}
|
||||
|
||||
Decompressor.Zip(tempZipFile)
|
||||
.removePrefixPath(rootFolderName)
|
||||
.extract(contentEntryDir)
|
||||
}
|
||||
|
||||
class DownloadResult(
|
||||
val isZip: Boolean,
|
||||
val tempFile: File,
|
||||
val filename: String
|
||||
)
|
||||
|
||||
internal class UnexpectedArchiveStructureException(message: String) : IOException(message)
|
||||
@@ -0,0 +1,518 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.ide.starters.remote.wizard
|
||||
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.ide.BrowserUtil
|
||||
import com.intellij.ide.starters.JavaStartersBundle
|
||||
import com.intellij.ide.starters.local.StarterModuleBuilder
|
||||
import com.intellij.ide.starters.remote.*
|
||||
import com.intellij.ide.starters.shared.*
|
||||
import com.intellij.ide.starters.shared.ValidationFunctions.*
|
||||
import com.intellij.ide.util.PropertiesComponent
|
||||
import com.intellij.ide.wizard.AbstractWizard
|
||||
import com.intellij.ide.wizard.withVisualPadding
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.application.ModalityState
|
||||
import com.intellij.openapi.diagnostic.logger
|
||||
import com.intellij.openapi.observable.properties.GraphProperty
|
||||
import com.intellij.openapi.observable.util.trim
|
||||
import com.intellij.openapi.progress.ProgressManager
|
||||
import com.intellij.openapi.roots.ui.configuration.validateJdkIntentVersion
|
||||
import com.intellij.openapi.ui.DialogPanel
|
||||
import com.intellij.openapi.ui.InputValidator
|
||||
import com.intellij.openapi.ui.Messages
|
||||
import com.intellij.openapi.ui.popup.IconButton
|
||||
import com.intellij.openapi.util.Condition
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.openapi.util.NlsSafe
|
||||
import com.intellij.openapi.util.Ref
|
||||
import com.intellij.openapi.util.io.FileUtil
|
||||
import com.intellij.ui.InplaceButton
|
||||
import com.intellij.ui.SimpleListCellRenderer
|
||||
import com.intellij.ui.components.ActionLink
|
||||
import com.intellij.ui.dsl.builder.*
|
||||
import com.intellij.util.concurrency.Semaphore
|
||||
import com.intellij.util.ui.AsyncProcessIcon
|
||||
import com.intellij.util.ui.UIUtil
|
||||
import java.awt.event.ActionListener
|
||||
import java.io.IOException
|
||||
import java.net.MalformedURLException
|
||||
import java.net.URL
|
||||
import java.util.concurrent.Future
|
||||
import javax.swing.DefaultComboBoxModel
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.SwingUtilities
|
||||
|
||||
open class WebStarterInitialStep(contextProvider: WebStarterContextProvider) : CommonStarterInitialStep(
|
||||
contextProvider.wizardContext,
|
||||
contextProvider.starterContext,
|
||||
contextProvider.moduleBuilder,
|
||||
contextProvider.parentDisposable,
|
||||
contextProvider.settings
|
||||
) {
|
||||
protected val moduleBuilder: WebStarterModuleBuilder = contextProvider.moduleBuilder
|
||||
private val starterContext: WebStarterContext = contextProvider.starterContext
|
||||
|
||||
private val packageNameProperty: GraphProperty<String> = propertyGraph.lazyProperty { starterContext.packageName }
|
||||
|
||||
private val packagingProperty: GraphProperty<StarterAppPackaging> = propertyGraph.lazyProperty {
|
||||
starterContext.packaging ?: StarterAppPackaging("unknown", "")
|
||||
}
|
||||
private val languageLevelProperty: GraphProperty<StarterLanguageLevel> = propertyGraph.lazyProperty {
|
||||
starterContext.languageLevel ?: StarterLanguageLevel("unknown", "", "")
|
||||
}
|
||||
|
||||
private var languageLevel: StarterLanguageLevel by languageLevelProperty
|
||||
private var packageName: String by packageNameProperty.trim()
|
||||
|
||||
private val contentPanel: DialogPanel by lazy { createComponent() }
|
||||
private val progressIcon: AsyncProcessIcon by lazy { AsyncProcessIcon(moduleBuilder.builderId + "ServerOptions") }
|
||||
private val serverUrlLink: ActionLink by lazy { createServerUrlLink() }
|
||||
private val retryButton: InplaceButton by lazy { createRetryButton() }
|
||||
|
||||
private val languageLevelsModel: DefaultComboBoxModel<StarterLanguageLevel> = DefaultComboBoxModel<StarterLanguageLevel>()
|
||||
private val applicationTypesModel: DefaultComboBoxModel<StarterAppType> = DefaultComboBoxModel<StarterAppType>()
|
||||
|
||||
private lateinit var projectTypesSelector: SegmentedButton<StarterProjectType>
|
||||
private lateinit var packagingTypesSelector: SegmentedButton<StarterAppPackaging>
|
||||
private lateinit var languagesSelector: SegmentedButton<StarterLanguage>
|
||||
|
||||
private var languages: List<StarterLanguage> = starterSettings.languages
|
||||
private var applicationTypes: List<StarterAppType> = starterSettings.applicationTypes
|
||||
private var projectTypes: List<StarterProjectType> = starterSettings.projectTypes
|
||||
private var packagingTypes: List<StarterAppPackaging> = starterSettings.packagingTypes
|
||||
|
||||
@Volatile
|
||||
private var serverOptions: WebStarterServerOptions? = null
|
||||
|
||||
@Volatile
|
||||
private var currentRequest: Future<*>? = null
|
||||
|
||||
@Volatile
|
||||
private var isDisposed: Boolean = false
|
||||
|
||||
private val serverOptionsLoadingSemaphore: Semaphore = Semaphore()
|
||||
private val serverSettingsButton: InplaceButton = InplaceButton(
|
||||
IconButton(JavaStartersBundle.message("button.tooltip.configure"), AllIcons.General.Gear, AllIcons.General.GearHover),
|
||||
ActionListener {
|
||||
configureServer()
|
||||
}
|
||||
)
|
||||
|
||||
init {
|
||||
Disposer.register(parentDisposable, Disposable {
|
||||
isDisposed = true
|
||||
currentRequest?.cancel(true)
|
||||
})
|
||||
}
|
||||
|
||||
override fun getComponent(): JComponent = contentPanel
|
||||
|
||||
override fun getHelpId(): String? = moduleBuilder.getHelpId()
|
||||
|
||||
override fun updateDataModel() {
|
||||
starterContext.serverOptions = this.serverOptions!!
|
||||
|
||||
starterContext.projectType = projectTypeProperty.get()
|
||||
starterContext.language = languageProperty.get()
|
||||
starterContext.group = groupId
|
||||
starterContext.artifact = artifactId
|
||||
starterContext.name = entityName
|
||||
starterContext.packageName = packageName
|
||||
starterContext.packaging = packagingProperty.get()
|
||||
starterContext.languageLevel = languageLevel
|
||||
starterContext.testFramework = testFrameworkProperty.get()
|
||||
starterContext.applicationType = applicationTypeProperty.get()
|
||||
starterContext.includeExamples = exampleCodeProperty.get()
|
||||
starterContext.gitIntegration = gitProperty.get()
|
||||
|
||||
wizardContext.projectName = entityName
|
||||
wizardContext.setProjectFileDirectory(FileUtil.join(location, entityName))
|
||||
|
||||
val sdk = jdkIntentProperty.get().prepareJdk()
|
||||
if (wizardContext.project == null) {
|
||||
wizardContext.projectJdk = sdk
|
||||
}
|
||||
else {
|
||||
moduleBuilder.moduleJdk = sdk
|
||||
}
|
||||
}
|
||||
|
||||
private fun suggestPackageName(): String {
|
||||
return StarterModuleBuilder.suggestPackageName(groupId, artifactId)
|
||||
}
|
||||
|
||||
private fun createComponent(): DialogPanel {
|
||||
entityNameProperty.dependsOn(artifactIdProperty) { artifactId }
|
||||
artifactIdProperty.dependsOn(entityNameProperty) { entityName }
|
||||
|
||||
packageNameProperty.dependsOn(artifactIdProperty, ::suggestPackageName)
|
||||
packageNameProperty.dependsOn(groupIdProperty, ::suggestPackageName)
|
||||
|
||||
progressIcon.toolTipText = JavaStartersBundle.message("message.state.connecting.and.retrieving.options")
|
||||
|
||||
return panel {
|
||||
row {
|
||||
label(JavaStartersBundle.message("title.project.server.url.label"))
|
||||
|
||||
cell(serverUrlLink)
|
||||
cell(serverSettingsButton)
|
||||
cell(retryButton)
|
||||
cell(progressIcon)
|
||||
}.bottomGap(BottomGap.SMALL)
|
||||
|
||||
addProjectLocationUi()
|
||||
|
||||
addFieldsBefore(this)
|
||||
|
||||
if (starterSettings.languages.size > 1) {
|
||||
row(JavaStartersBundle.message("title.project.language.label")) {
|
||||
languagesSelector = segmentedButton(starterSettings.languages) { text = it.title }
|
||||
.bind(languageProperty)
|
||||
|
||||
bottomGap(BottomGap.SMALL)
|
||||
}
|
||||
}
|
||||
|
||||
if (starterSettings.projectTypes.isNotEmpty() && starterSettings.showProjectTypes) {
|
||||
val messages = starterSettings.customizedMessages
|
||||
row(messages?.projectTypeLabel ?: JavaStartersBundle.message("title.project.type.label")) {
|
||||
projectTypesSelector = segmentedButton(starterSettings.projectTypes) {
|
||||
text = it.title
|
||||
toolTipText = it.description
|
||||
}.bind(projectTypeProperty)
|
||||
|
||||
bottomGap(BottomGap.SMALL)
|
||||
}
|
||||
}
|
||||
|
||||
if (starterSettings.testFrameworks.isNotEmpty()) {
|
||||
row(JavaStartersBundle.message("title.project.test.framework.label")) {
|
||||
segmentedButton(starterSettings.testFrameworks) { text = it.title }
|
||||
.bind(testFrameworkProperty)
|
||||
}.bottomGap(BottomGap.SMALL)
|
||||
}
|
||||
|
||||
addGroupArtifactUi()
|
||||
|
||||
if (starterSettings.isPackageNameEditable) {
|
||||
row(JavaStartersBundle.message("title.project.package.label")) {
|
||||
textField()
|
||||
.accessibleName(JavaStartersBundle.message("title.project.package.label"))
|
||||
.bindText(packageNameProperty)
|
||||
.columns(COLUMNS_MEDIUM)
|
||||
.withSpecialValidation(CHECK_NOT_EMPTY, CHECK_NO_WHITESPACES, CHECK_NO_RESERVED_WORDS, CHECK_PACKAGE_NAME)
|
||||
}.bottomGap(BottomGap.SMALL)
|
||||
}
|
||||
|
||||
if (starterSettings.applicationTypes.isNotEmpty()) {
|
||||
row(JavaStartersBundle.message("title.project.app.type.label")) {
|
||||
applicationTypesModel.addAll(starterSettings.applicationTypes)
|
||||
comboBox(applicationTypesModel, SimpleListCellRenderer.create("") { it?.title ?: "" })
|
||||
.bindItem(applicationTypeProperty)
|
||||
.columns(COLUMNS_MEDIUM)
|
||||
}.bottomGap(BottomGap.SMALL)
|
||||
}
|
||||
|
||||
addSdkUi()
|
||||
|
||||
if (starterSettings.languageLevels.isNotEmpty()) {
|
||||
row(JavaStartersBundle.message("title.project.java.version.label")) {
|
||||
languageLevelsModel.addAll(starterSettings.languageLevels)
|
||||
comboBox(languageLevelsModel, SimpleListCellRenderer.create("") { it?.title ?: "" })
|
||||
.bindItem(languageLevelProperty)
|
||||
}.bottomGap(BottomGap.SMALL)
|
||||
}
|
||||
|
||||
if (starterSettings.packagingTypes.isNotEmpty()) {
|
||||
row(JavaStartersBundle.message("title.project.packaging.label")) {
|
||||
packagingTypesSelector = segmentedButton(starterSettings.packagingTypes) { text = it.title }
|
||||
.bind(packagingProperty)
|
||||
}.bottomGap(BottomGap.SMALL)
|
||||
}
|
||||
|
||||
addSampleCodeUi()
|
||||
|
||||
addFieldsAfter(this)
|
||||
}.withVisualPadding()
|
||||
}
|
||||
|
||||
private fun createServerUrlLink(): ActionLink {
|
||||
val result = ActionLink(urlPreview(starterContext.serverUrl)) {
|
||||
BrowserUtil.browse(starterContext.serverUrl)
|
||||
}
|
||||
UIUtil.applyStyle(UIUtil.ComponentStyle.REGULAR, result)
|
||||
return result
|
||||
}
|
||||
|
||||
private fun createRetryButton(): InplaceButton {
|
||||
return InplaceButton(IconButton(JavaStartersBundle.message("button.tooltip.retry"),
|
||||
AllIcons.Nodes.ErrorIntroduction, AllIcons.Actions.ForceRefresh), ActionListener {
|
||||
requestServerOptions()
|
||||
}).apply {
|
||||
isVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
@NlsSafe
|
||||
private fun urlPreview(serverUrl: String): String {
|
||||
val url = serverUrl
|
||||
.removePrefix("https://")
|
||||
.removeSuffix("/")
|
||||
|
||||
if (url.length > 35) {
|
||||
return url.take(30) + "..."
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
override fun getPreferredFocusedComponent(): JComponent? {
|
||||
return contentPanel.preferredFocusedComponent
|
||||
}
|
||||
|
||||
override fun validate(): Boolean {
|
||||
if (!validateFormFields(component, contentPanel, validatedTextComponents)) {
|
||||
return false
|
||||
}
|
||||
val passTechnologyName = if (starterSettings.languageLevels.size > 1) null else moduleBuilder.presentableName
|
||||
if (languageLevel.javaVersion.isNotBlank() &&
|
||||
!validateJdkIntentVersion(jdkIntentProperty, languageLevel.javaVersion, passTechnologyName)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return checkServerOptionsLoaded()
|
||||
}
|
||||
|
||||
private fun checkServerOptionsLoaded(): Boolean {
|
||||
val request = currentRequest
|
||||
if (serverOptions != null && request == null) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (request == null) {
|
||||
// failure? retry server options loading
|
||||
requestServerOptions()
|
||||
}
|
||||
|
||||
val newOptionsRef: Ref<WebStarterServerOptions> = Ref.create()
|
||||
|
||||
ProgressManager.getInstance().runProcessWithProgressSynchronously(Runnable {
|
||||
val progressIndicator = ProgressManager.getInstance().progressIndicator
|
||||
progressIndicator.isIndeterminate = true
|
||||
|
||||
for (i in 0 until 30) {
|
||||
progressIndicator.checkCanceled()
|
||||
if (serverOptionsLoadingSemaphore.waitFor(500)) {
|
||||
serverOptions?.let {
|
||||
newOptionsRef.set(it)
|
||||
}
|
||||
return@Runnable
|
||||
}
|
||||
}
|
||||
}, JavaStartersBundle.message("message.state.connecting.and.retrieving.options"), true, wizardContext.project)
|
||||
|
||||
if (!newOptionsRef.isNull) {
|
||||
updatePropertiesWithServerOptions(newOptionsRef.get())
|
||||
}
|
||||
|
||||
return serverOptions != null
|
||||
}
|
||||
|
||||
override fun _init() {
|
||||
super._init()
|
||||
|
||||
if (serverOptions == null && currentRequest == null) {
|
||||
val serverUrlFromSettings = PropertiesComponent.getInstance().getValue(getServerUrlPropertyName())
|
||||
if (serverUrlFromSettings != null) {
|
||||
setServerUrl(serverUrlFromSettings)
|
||||
}
|
||||
|
||||
// required on dialog opening to get correct modality state
|
||||
SwingUtilities.invokeLater(this::requestServerOptions)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setServerUrl(@NlsSafe url: String) {
|
||||
starterContext.serverUrl = url
|
||||
serverUrlLink.text = urlPreview(url)
|
||||
serverUrlLink.toolTipText = url
|
||||
}
|
||||
|
||||
private fun requestServerOptions() {
|
||||
progressIcon.isVisible = true
|
||||
retryButton.isVisible = false
|
||||
progressIcon.resume()
|
||||
|
||||
serverOptionsLoadingSemaphore.down()
|
||||
|
||||
currentRequest = ApplicationManager.getApplication().executeOnPooledThread {
|
||||
val readyServerOptions = try {
|
||||
moduleBuilder.getServerOptions(starterContext.serverUrl)
|
||||
}
|
||||
catch (e: Exception) {
|
||||
if (e is IOException || e is IllegalStateException) {
|
||||
logger<WebStarterInitialStep>().info("Unable to get server options for " + moduleBuilder.builderId, e)
|
||||
}
|
||||
else {
|
||||
logger<WebStarterInitialStep>().error("Unable to get server options for " + moduleBuilder.builderId, e)
|
||||
}
|
||||
|
||||
ApplicationManager.getApplication().invokeLater(
|
||||
{
|
||||
if (component.isShowing) {
|
||||
// only if the wizard is visible
|
||||
Messages.showErrorDialog(
|
||||
JavaStartersBundle.message("message.no.connection.with.error.content", starterContext.serverUrl, e.message),
|
||||
JavaStartersBundle.message("message.title.error"))
|
||||
}
|
||||
}, getModalityState())
|
||||
|
||||
null
|
||||
}
|
||||
|
||||
setServerOptions(readyServerOptions)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setServerOptions(serverOptions: WebStarterServerOptions?) {
|
||||
this.serverOptions = serverOptions
|
||||
this.currentRequest = null
|
||||
this.serverOptionsLoadingSemaphore.up()
|
||||
|
||||
ApplicationManager.getApplication().invokeLater(Runnable {
|
||||
progressIcon.suspend()
|
||||
progressIcon.isVisible = false
|
||||
retryButton.isVisible = serverOptions == null
|
||||
|
||||
if (serverOptions != null) {
|
||||
updatePropertiesWithServerOptions(serverOptions)
|
||||
}
|
||||
}, getModalityState(), getDisposed())
|
||||
}
|
||||
|
||||
private fun getModalityState(): ModalityState {
|
||||
return ModalityState.stateForComponent(wizardContext.getUserData(AbstractWizard.KEY)!!.contentComponent)
|
||||
}
|
||||
|
||||
private fun getDisposed(): Condition<Any> = Condition<Any> { isDisposed }
|
||||
|
||||
private fun configureServer() {
|
||||
val currentServerUrl = starterContext.serverUrl
|
||||
val serverUrlTitle = starterSettings.customizedMessages?.serverUrlDialogTitle
|
||||
?: JavaStartersBundle.message("title.server.url.dialog")
|
||||
|
||||
val newUrl = Messages.showInputDialog(component, null, serverUrlTitle, null, currentServerUrl, object : InputValidator {
|
||||
override fun canClose(inputString: String?): Boolean = checkInput(inputString)
|
||||
|
||||
override fun checkInput(inputString: String?): Boolean {
|
||||
try {
|
||||
URL(inputString)
|
||||
}
|
||||
catch (e: MalformedURLException) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
// update
|
||||
if (newUrl != null && starterContext.serverUrl != newUrl) {
|
||||
setServerUrl(newUrl)
|
||||
|
||||
PropertiesComponent.getInstance().setValue(getServerUrlPropertyName(), newUrl)
|
||||
|
||||
requestServerOptions()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getServerUrlPropertyName(): String {
|
||||
return moduleBuilder.builderId + ".service.url.last"
|
||||
}
|
||||
|
||||
protected open fun updatePropertiesWithServerOptions(serverOptions: WebStarterServerOptions) {
|
||||
starterContext.frameworkVersion = serverOptions.frameworkVersions.find { it.isDefault }
|
||||
?: serverOptions.frameworkVersions.firstOrNull()
|
||||
|
||||
val currentPackageName = packageName // remember package name before applying group and artifact
|
||||
|
||||
serverOptions.extractOption(SERVER_NAME_KEY) {
|
||||
if (entityName == suggestName(DEFAULT_MODULE_NAME)) {
|
||||
val newName = suggestName(it)
|
||||
if (entityName != newName) {
|
||||
entityNameProperty.set(newName)
|
||||
}
|
||||
}
|
||||
}
|
||||
serverOptions.extractOption(SERVER_GROUP_KEY) {
|
||||
if (groupId == DEFAULT_MODULE_GROUP && groupId != it) {
|
||||
groupIdProperty.set(it)
|
||||
}
|
||||
}
|
||||
serverOptions.extractOption(SERVER_ARTIFACT_KEY) {
|
||||
if (artifactId == DEFAULT_MODULE_ARTIFACT && artifactId != it) {
|
||||
artifactIdProperty.set(it)
|
||||
}
|
||||
}
|
||||
serverOptions.extractOption(SERVER_PACKAGE_NAME_KEY) {
|
||||
if (currentPackageName == DEFAULT_PACKAGE_NAME && currentPackageName != it) {
|
||||
packageNameProperty.set(it)
|
||||
}
|
||||
}
|
||||
serverOptions.extractOption(SERVER_VERSION_KEY) {
|
||||
starterContext.version = it
|
||||
}
|
||||
serverOptions.extractOption(SERVER_LANGUAGE_LEVELS_KEY) { levels ->
|
||||
val selectedItem = languageLevelsModel.selectedItem
|
||||
languageLevelsModel.removeAllElements()
|
||||
languageLevelsModel.addAll(levels)
|
||||
|
||||
if (levels.contains(selectedItem)) {
|
||||
languageLevelsModel.selectedItem = selectedItem
|
||||
}
|
||||
else {
|
||||
languageLevel = levels.firstOrNull() ?: StarterLanguageLevel("unknown", "", "")
|
||||
languageLevelsModel.selectedItem = languageLevel
|
||||
}
|
||||
}
|
||||
serverOptions.extractOption(SERVER_LANGUAGE_LEVEL_KEY) { level ->
|
||||
if (languageLevel == starterSettings.defaultLanguageLevel && languageLevel != level) {
|
||||
languageLevelProperty.set(level)
|
||||
}
|
||||
}
|
||||
serverOptions.extractOption(SERVER_PROJECT_TYPES) { types ->
|
||||
if (types.isNotEmpty() && types != this.projectTypes && ::projectTypesSelector.isInitialized) {
|
||||
val correspondingOption = types.find { it.id == projectTypeProperty.get().id }
|
||||
projectTypeProperty.set(correspondingOption ?: types.first())
|
||||
projectTypesSelector.items = types
|
||||
this.projectTypes = types
|
||||
}
|
||||
}
|
||||
serverOptions.extractOption(SERVER_APPLICATION_TYPES) { types ->
|
||||
if (types.isNotEmpty() && types != applicationTypes) {
|
||||
applicationTypesModel.removeAllElements()
|
||||
applicationTypesModel.addAll(types)
|
||||
applicationTypesModel.selectedItem = types.firstOrNull()
|
||||
this.applicationTypes = types
|
||||
}
|
||||
}
|
||||
serverOptions.extractOption(SERVER_PACKAGING_TYPES) { types ->
|
||||
if (types.isNotEmpty() && types != this.packagingTypes && ::packagingTypesSelector.isInitialized) {
|
||||
val correspondingOption = types.find { it.id == packagingProperty.get().id }
|
||||
packagingProperty.set(correspondingOption ?: types.first())
|
||||
packagingTypesSelector.items = types
|
||||
this.packagingTypes = types
|
||||
}
|
||||
}
|
||||
serverOptions.extractOption(SERVER_LANGUAGES) { languages ->
|
||||
if (languages.isNotEmpty() && languages != this.languages && ::languagesSelector.isInitialized) {
|
||||
val correspondingOption = languages.find { it.id == languageProperty.get().id }
|
||||
languageProperty.set(correspondingOption ?: languages.first())
|
||||
languagesSelector.items = languages
|
||||
this.languages = languages
|
||||
}
|
||||
}
|
||||
|
||||
contentPanel.revalidate()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,488 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.ide.starters.remote.wizard
|
||||
|
||||
import com.intellij.ide.IdeBundle
|
||||
import com.intellij.ide.starters.JavaStartersBundle
|
||||
import com.intellij.ide.starters.remote.*
|
||||
import com.intellij.ide.starters.shared.*
|
||||
import com.intellij.ide.starters.shared.ui.LibrariesSearchTextField
|
||||
import com.intellij.ide.starters.shared.ui.LibraryDescriptionPanel
|
||||
import com.intellij.ide.starters.shared.ui.SelectedLibrariesPanel
|
||||
import com.intellij.ide.util.projectWizard.ModuleWizardStep
|
||||
import com.intellij.ide.util.projectWizard.WizardContext
|
||||
import com.intellij.ide.wizard.AbstractWizard
|
||||
import com.intellij.ide.wizard.withVisualPadding
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.application.ModalityState
|
||||
import com.intellij.openapi.diagnostic.logger
|
||||
import com.intellij.openapi.observable.properties.GraphProperty
|
||||
import com.intellij.openapi.observable.properties.PropertyGraph
|
||||
import com.intellij.openapi.progress.ProgressIndicator
|
||||
import com.intellij.openapi.progress.ProgressManager
|
||||
import com.intellij.openapi.ui.DialogPanel
|
||||
import com.intellij.openapi.ui.Messages
|
||||
import com.intellij.openapi.util.Condition
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.openapi.util.text.StringUtil
|
||||
import com.intellij.ui.*
|
||||
import com.intellij.ui.components.JBLabel
|
||||
import com.intellij.ui.dsl.builder.*
|
||||
import com.intellij.util.ModalityUiUtil
|
||||
import com.intellij.util.concurrency.EdtExecutorService
|
||||
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
|
||||
import com.intellij.util.ui.JBUI
|
||||
import com.intellij.util.ui.UIUtil.DEFAULT_HGAP
|
||||
import com.intellij.util.ui.UIUtil.DEFAULT_VGAP
|
||||
import com.intellij.util.ui.components.BorderLayoutPanel
|
||||
import com.intellij.util.ui.tree.TreeUtil
|
||||
import com.intellij.util.ui.update.MergingUpdateQueue
|
||||
import com.intellij.util.ui.update.Update
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Dimension
|
||||
import java.awt.GridBagLayout
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.swing.DefaultComboBoxModel
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JPanel
|
||||
import javax.swing.JTree
|
||||
import javax.swing.event.DocumentEvent
|
||||
import javax.swing.event.ListDataEvent
|
||||
import javax.swing.event.ListDataListener
|
||||
import javax.swing.event.TreeSelectionListener
|
||||
import javax.swing.tree.DefaultMutableTreeNode
|
||||
import javax.swing.tree.DefaultTreeModel
|
||||
import javax.swing.tree.TreeSelectionModel
|
||||
|
||||
open class WebStarterLibrariesStep(contextProvider: WebStarterContextProvider) : ModuleWizardStep() {
|
||||
protected val moduleBuilder: WebStarterModuleBuilder = contextProvider.moduleBuilder
|
||||
private val wizardContext: WizardContext = contextProvider.wizardContext
|
||||
protected val starterContext: WebStarterContext = contextProvider.starterContext
|
||||
private val starterSettings: StarterWizardSettings = contextProvider.settings
|
||||
protected val parentDisposable: Disposable = contextProvider.parentDisposable
|
||||
|
||||
private val topLevelPanel: JPanel = BorderLayoutPanel()
|
||||
private val contentPanel: DialogPanel by lazy { createComponent() }
|
||||
|
||||
private val librariesList: CheckboxTreeBase by lazy { createLibrariesList() }
|
||||
private val librariesSearchField: LibrariesSearchTextField by lazy { createLibrariesFilter() }
|
||||
private val libraryDescriptionPanel: LibraryDescriptionPanel by lazy { LibraryDescriptionPanel() }
|
||||
private val selectedLibrariesPanel: SelectedLibrariesPanel by lazy { createSelectedLibrariesPanel() }
|
||||
private val frameworkVersionsModel: DefaultComboBoxModel<WebStarterFrameworkVersion> = DefaultComboBoxModel()
|
||||
|
||||
protected val propertyGraph: PropertyGraph = PropertyGraph()
|
||||
private val frameworkVersionProperty: GraphProperty<WebStarterFrameworkVersion?> = propertyGraph.property(null)
|
||||
private val selectedDependencies: MutableSet<WebStarterDependency> = mutableSetOf()
|
||||
|
||||
private var currentSearchString: String = ""
|
||||
private val searchMergingUpdateQueue: MergingUpdateQueue by lazy {
|
||||
MergingUpdateQueue("SearchLibs_" + moduleBuilder.builderId, 250, true, topLevelPanel, parentDisposable)
|
||||
}
|
||||
|
||||
override fun getComponent(): JComponent = topLevelPanel
|
||||
|
||||
override fun getHelpId(): String? = moduleBuilder.getHelpId()
|
||||
|
||||
override fun getPreferredFocusedComponent(): JComponent? = librariesSearchField
|
||||
|
||||
override fun updateDataModel() {
|
||||
starterContext.frameworkVersion = frameworkVersionProperty.get()
|
||||
starterContext.dependencies.clear()
|
||||
starterContext.dependencies.addAll(selectedDependencies)
|
||||
}
|
||||
|
||||
override fun onStepLeaving() {
|
||||
super.onStepLeaving()
|
||||
|
||||
updateDataModel()
|
||||
}
|
||||
|
||||
override fun _init() {
|
||||
super._init()
|
||||
|
||||
if (topLevelPanel.componentCount == 0) {
|
||||
topLevelPanel.add(contentPanel, BorderLayout.CENTER)
|
||||
}
|
||||
|
||||
if (topLevelPanel.isDisplayable && topLevelPanel.isShowing) {
|
||||
// called after unsuccessful validation of step
|
||||
return
|
||||
}
|
||||
|
||||
// libraries list may depend on options specified on the first step
|
||||
loadLibrariesList()
|
||||
loadFrameworkVersions()
|
||||
|
||||
updateAvailableDependencies()
|
||||
getLibrariesRoot()?.let {
|
||||
selectFirstDependency(it)
|
||||
}
|
||||
}
|
||||
|
||||
final override fun validate(): Boolean {
|
||||
val unavailable = selectedDependencies.filter { getDependencyState(it) is DependencyUnavailable }
|
||||
if (unavailable.isNotEmpty()) {
|
||||
val dependencyInfo = unavailable.joinToString { it.title }
|
||||
val version = frameworkVersionProperty.get()?.title ?: ""
|
||||
Messages.showErrorDialog(
|
||||
JavaStartersBundle.message("message.unavailable.dependencies", dependencyInfo, version),
|
||||
JavaStartersBundle.message("message.title.error"))
|
||||
return false
|
||||
}
|
||||
|
||||
if (!validateFields()) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (starterContext.result == null) {
|
||||
// commit selected dependencies to starterContext
|
||||
updateDataModel()
|
||||
|
||||
// try to validate and download result
|
||||
requestWebService()
|
||||
|
||||
if (starterContext.result == null) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
protected open fun validateFields(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
@RequiresBackgroundThread
|
||||
protected open fun validateWithServer(progressIndicator: ProgressIndicator): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
private fun requestWebService() {
|
||||
ProgressManager.getInstance().runProcessWithProgressSynchronously(
|
||||
{
|
||||
val progressIndicator = ProgressManager.getInstance().progressIndicator
|
||||
|
||||
if (!validateWithServer(progressIndicator)) {
|
||||
return@runProcessWithProgressSynchronously
|
||||
}
|
||||
|
||||
progressIndicator.checkCanceled()
|
||||
|
||||
progressIndicator.text = JavaStartersBundle.message("message.state.downloading.template", moduleBuilder.presentableName)
|
||||
|
||||
val downloadResult: DownloadResult? = try {
|
||||
moduleBuilder.downloadResultInternal(progressIndicator)
|
||||
}
|
||||
catch (e: Exception) {
|
||||
logger<WebStarterLibrariesStep>().info(e)
|
||||
|
||||
EdtExecutorService.getScheduledExecutorInstance().schedule(
|
||||
{
|
||||
var message = JavaStartersBundle.message("error.text.with.error.content", e.message)
|
||||
message = StringUtil.shortenTextWithEllipsis(message, 1024, 0) // exactly 1024 because why not
|
||||
Messages.showErrorDialog(message, moduleBuilder.presentableName)
|
||||
},
|
||||
3, TimeUnit.SECONDS)
|
||||
|
||||
null
|
||||
}
|
||||
|
||||
starterContext.result = downloadResult
|
||||
}, JavaStartersBundle.message("message.state.preparing.template"), true, wizardContext.project)
|
||||
}
|
||||
|
||||
private fun loadFrameworkVersions() {
|
||||
val availableFrameworkVersions = getAvailableFrameworkVersions()
|
||||
frameworkVersionsModel.removeAllElements()
|
||||
frameworkVersionsModel.addAll(availableFrameworkVersions)
|
||||
val defaultVersion = starterContext.frameworkVersion ?: availableFrameworkVersions.firstOrNull()
|
||||
if (availableFrameworkVersions.contains(defaultVersion)) {
|
||||
frameworkVersionProperty.set(defaultVersion)
|
||||
}
|
||||
else {
|
||||
frameworkVersionProperty.set(availableFrameworkVersions.firstOrNull())
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun Panel.addFieldsAfter() {}
|
||||
|
||||
private fun createComponent(): DialogPanel {
|
||||
val messages = starterSettings.customizedMessages
|
||||
selectedLibrariesPanel.emptyText.text = messages?.noDependenciesSelectedLabel
|
||||
?: JavaStartersBundle.message("hint.dependencies.not.selected")
|
||||
|
||||
return panel {
|
||||
val frameworkVersions = getAvailableFrameworkVersions()
|
||||
if (frameworkVersions.isNotEmpty()) {
|
||||
row(messages?.frameworkVersionLabel ?: JavaStartersBundle.message("title.project.version.label")) {
|
||||
if (frameworkVersions.size == 1) {
|
||||
label(frameworkVersions[0].title)
|
||||
}
|
||||
else {
|
||||
frameworkVersionsModel.addListDataListener(object : ListDataListener {
|
||||
override fun intervalAdded(e: ListDataEvent?) = updateAvailableDependencies()
|
||||
override fun intervalRemoved(e: ListDataEvent?) = updateAvailableDependencies()
|
||||
override fun contentsChanged(e: ListDataEvent?) = updateAvailableDependencies()
|
||||
})
|
||||
comboBox(frameworkVersionsModel, SimpleListCellRenderer.create("") { it?.title ?: "" })
|
||||
.bindItem(frameworkVersionProperty)
|
||||
}
|
||||
}.bottomGap(BottomGap.SMALL)
|
||||
}
|
||||
|
||||
addFieldsAfter()
|
||||
|
||||
row {
|
||||
label(messages?.dependenciesLabel ?: JavaStartersBundle.message("title.project.dependencies.label"))
|
||||
.applyToComponent { labelFor = librariesList }
|
||||
}
|
||||
|
||||
row {
|
||||
cell(JPanel(GridBagLayout()).apply {
|
||||
add(BorderLayoutPanel().apply {
|
||||
preferredSize = Dimension(0, 0)
|
||||
|
||||
addToTop(librariesSearchField)
|
||||
addToCenter(ScrollPaneFactory.createScrollPane(librariesList))
|
||||
}, gridConstraint(0, 0))
|
||||
|
||||
add(JPanel(GridBagLayout()).apply {
|
||||
border = JBUI.Borders.emptyLeft(DEFAULT_HGAP * 2)
|
||||
preferredSize = Dimension(0, 0)
|
||||
|
||||
add(libraryDescriptionPanel.apply {
|
||||
preferredSize = Dimension(0, 0)
|
||||
}, gridConstraint(0, 0))
|
||||
add(BorderLayoutPanel().apply {
|
||||
preferredSize = Dimension(0, 0)
|
||||
|
||||
addToTop(JBLabel(messages?.selectedDependenciesLabel
|
||||
?: JavaStartersBundle.message("title.project.dependencies.selected.label")).apply {
|
||||
border = JBUI.Borders.emptyBottom(DEFAULT_VGAP * 2)
|
||||
})
|
||||
addToCenter(selectedLibrariesPanel)
|
||||
}, gridConstraint(0, 1))
|
||||
}, gridConstraint(1, 0))
|
||||
}).resizableColumn()
|
||||
.align(Align.FILL)
|
||||
}.resizableRow()
|
||||
}.withVisualPadding()
|
||||
}
|
||||
|
||||
private fun getAvailableFrameworkVersions(): List<WebStarterFrameworkVersion> {
|
||||
return starterContext.serverOptions.frameworkVersions.filter {
|
||||
moduleBuilder.isVersionAvailableInternal(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createLibrariesList(): CheckboxTreeBase {
|
||||
val list = CheckboxTreeBase(object : CheckboxTree.CheckboxTreeCellRenderer() {
|
||||
override fun customizeRenderer(
|
||||
tree: JTree,
|
||||
value: Any,
|
||||
selected: Boolean,
|
||||
expanded: Boolean,
|
||||
leaf: Boolean,
|
||||
row: Int,
|
||||
hasFocus: Boolean,
|
||||
) {
|
||||
if (value !is DefaultMutableTreeNode) return
|
||||
|
||||
this.border = JBUI.Borders.empty(2, 0)
|
||||
when (val item = value.userObject) {
|
||||
is WebStarterDependencyCategory -> textRenderer.append(item.title)
|
||||
is WebStarterDependency -> {
|
||||
val enabled = (value as CheckedTreeNode).isEnabled
|
||||
val attributes = if (enabled) SimpleTextAttributes.REGULAR_ATTRIBUTES else SimpleTextAttributes.GRAYED_ATTRIBUTES
|
||||
textRenderer.append(item.title, attributes)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, null)
|
||||
list.emptyText.text = IdeBundle.message("empty.text.nothing.found")
|
||||
|
||||
enableEnterKeyHandling(list)
|
||||
|
||||
list.rowHeight = 0
|
||||
list.isRootVisible = false
|
||||
list.selectionModel.selectionMode = TreeSelectionModel.SINGLE_TREE_SELECTION
|
||||
|
||||
list.addCheckboxTreeListener(object : CheckboxTreeListener {
|
||||
override fun nodeStateChanged(node: CheckedTreeNode) {
|
||||
val dependency = node.userObject as? WebStarterDependency ?: return
|
||||
if (node.isChecked) {
|
||||
selectedDependencies.add(dependency)
|
||||
}
|
||||
else {
|
||||
selectedDependencies.remove(dependency)
|
||||
}
|
||||
librariesList.repaint()
|
||||
selectedLibrariesPanel.update(selectedDependencies)
|
||||
}
|
||||
})
|
||||
list.selectionModel.addTreeSelectionListener(TreeSelectionListener { e ->
|
||||
val path = e.path
|
||||
if (path != null && e.isAddedPath) {
|
||||
when (val item = (path.lastPathComponent as? DefaultMutableTreeNode)?.userObject) {
|
||||
is WebStarterDependency -> {
|
||||
updateSelectedLibraryInfo(item)
|
||||
}
|
||||
is WebStarterDependencyCategory -> libraryDescriptionPanel.update(item.title, null)
|
||||
}
|
||||
}
|
||||
else {
|
||||
libraryDescriptionPanel.reset()
|
||||
}
|
||||
})
|
||||
librariesSearchField.list = list
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
private fun isDependencyMatched(item: WebStarterDependency, search: String): Boolean {
|
||||
return item.title.contains(search, true)
|
||||
|| (item.description ?: "").contains(search, true)
|
||||
|| item.id.contains(search, true)
|
||||
}
|
||||
|
||||
private fun createLibrariesFilter(): LibrariesSearchTextField {
|
||||
val textField = LibrariesSearchTextField()
|
||||
textField.addDocumentListener(object : DocumentAdapter() {
|
||||
override fun textChanged(e: DocumentEvent) {
|
||||
searchMergingUpdateQueue.queue(Update.create("", Runnable {
|
||||
ModalityUiUtil.invokeLaterIfNeeded(getModalityState(), Runnable {
|
||||
currentSearchString = textField.text
|
||||
loadLibrariesList()
|
||||
librariesList.repaint()
|
||||
})
|
||||
}))
|
||||
}
|
||||
})
|
||||
return textField
|
||||
}
|
||||
|
||||
protected open fun getModalityState(): ModalityState {
|
||||
return ModalityState.stateForComponent(wizardContext.getUserData(AbstractWizard.KEY)!!.contentComponent)
|
||||
}
|
||||
|
||||
protected fun getDisposed(): Condition<Any> = Condition<Any> { Disposer.isDisposed(parentDisposable) }
|
||||
|
||||
protected open fun createSelectedLibrariesPanel(): SelectedLibrariesPanel {
|
||||
val panel = SelectedLibrariesPanel()
|
||||
val messages = starterSettings.customizedMessages
|
||||
panel.emptyText.text = messages?.noDependenciesSelectedLabel ?: JavaStartersBundle.message("hint.dependencies.not.selected")
|
||||
panel.libraryRemoveListener = { libraryInfo ->
|
||||
selectedDependencies.remove(libraryInfo)
|
||||
walkCheckedTree(getLibrariesRoot()) {
|
||||
if (it.userObject == libraryInfo) {
|
||||
librariesList.setNodeState(it, false)
|
||||
}
|
||||
}
|
||||
selectedLibrariesPanel.update(selectedDependencies)
|
||||
}
|
||||
if (starterContext.frameworkVersion != null) {
|
||||
panel.dependencyStateFunction = { libraryInfo ->
|
||||
getDependencyState(libraryInfo)
|
||||
}
|
||||
}
|
||||
|
||||
return panel
|
||||
}
|
||||
|
||||
private fun getLibrariesRoot(): CheckedTreeNode? {
|
||||
return librariesList.model.root as? CheckedTreeNode
|
||||
}
|
||||
|
||||
private fun getDependencyState(libraryInfo: LibraryInfo): DependencyState {
|
||||
val frameworkVersion = frameworkVersionProperty.get() ?: return DependencyAvailable
|
||||
return moduleBuilder.getDependencyStateInternal(frameworkVersion, libraryInfo as WebStarterDependency)
|
||||
}
|
||||
|
||||
private fun loadLibrariesList() {
|
||||
val librariesRoot = CheckedTreeNode()
|
||||
val search = currentSearchString.trim()
|
||||
|
||||
val dependencyCategories = starterContext.serverOptions.dependencyCategories
|
||||
for (category in dependencyCategories) {
|
||||
if (!category.isAvailable(starterContext)) continue
|
||||
|
||||
val categoryNode = DefaultMutableTreeNode(category, true)
|
||||
|
||||
for (dependency in category.dependencies) {
|
||||
if (search.isBlank() || isDependencyMatched(dependency, search)) {
|
||||
val libraryNode = CheckedTreeNode(dependency)
|
||||
if (dependency.isDefault) {
|
||||
selectedDependencies.add(dependency)
|
||||
}
|
||||
|
||||
libraryNode.isChecked = selectedDependencies.contains(dependency)
|
||||
|
||||
val state = getDependencyState(dependency)
|
||||
libraryNode.isEnabled = !dependency.isDefault && state is DependencyAvailable
|
||||
|
||||
if (dependencyCategories.size > 1) {
|
||||
categoryNode.add(libraryNode)
|
||||
}
|
||||
else {
|
||||
librariesRoot.add(libraryNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (dependencyCategories.size > 1) {
|
||||
if (categoryNode.childCount > 0) {
|
||||
librariesRoot.add(categoryNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
librariesList.model = DefaultTreeModel(librariesRoot)
|
||||
|
||||
if (search.isNotBlank()) {
|
||||
for (category in librariesRoot.children()) {
|
||||
librariesList.expandPath(TreeUtil.getPath(librariesRoot, category))
|
||||
}
|
||||
selectFirstDependency(librariesRoot)
|
||||
}
|
||||
}
|
||||
|
||||
private fun selectFirstDependency(librariesRoot: CheckedTreeNode) {
|
||||
if (librariesRoot.childCount > 0) {
|
||||
val firstNode = librariesRoot.getChildAt(0)
|
||||
if (firstNode is CheckedTreeNode) {
|
||||
librariesList.selectionModel.addSelectionPath(TreeUtil.getPath(librariesRoot, firstNode))
|
||||
}
|
||||
else {
|
||||
librariesList.expandPath(TreeUtil.getPath(librariesRoot, firstNode))
|
||||
if (firstNode.childCount > 0) {
|
||||
librariesList.selectionModel.addSelectionPath(TreeUtil.getPath(librariesRoot, firstNode.getChildAt(0)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateSelectedLibraryInfo(item: WebStarterDependency) {
|
||||
val dependencyState = getDependencyState(item)
|
||||
val versionInfo = if (dependencyState is DependencyUnavailable) dependencyState.hint else null
|
||||
libraryDescriptionPanel.update(item, versionInfo)
|
||||
}
|
||||
|
||||
private fun updateAvailableDependencies() {
|
||||
selectedLibrariesPanel.update(selectedDependencies)
|
||||
|
||||
val root = getLibrariesRoot() ?: return
|
||||
|
||||
walkCheckedTree(root) {
|
||||
val dependency = it.userObject as? WebStarterDependency
|
||||
if (dependency != null) {
|
||||
val state = getDependencyState(dependency)
|
||||
it.isEnabled = !dependency.isDefault && state is DependencyAvailable
|
||||
}
|
||||
}
|
||||
librariesList.repaint()
|
||||
|
||||
val selectedDependency = (librariesList.selectionPath?.lastPathComponent as? CheckedTreeNode)?.userObject
|
||||
if (selectedDependency is WebStarterDependency) {
|
||||
updateSelectedLibraryInfo(selectedDependency)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user