Frameworks: move starters APIs for New Project wizards to Java plugin

GitOrigin-RevId: e78209d9faf420b38a9d695d2653bfaae43d146c
This commit is contained in:
Yuriy Artamonov
2021-04-29 17:49:26 +03:00
committed by intellij-monorepo-bot
parent 8be8af8a94
commit e898ecf93f
42 changed files with 4615 additions and 8 deletions

1
java/idea-ui/.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
*.bin binary

View File

@@ -30,5 +30,6 @@
<orderEntry type="module" module-name="intellij.platform.core.ui" />
<orderEntry type="module" module-name="intellij.platform.workspaceModel.ide" />
<orderEntry type="module" module-name="intellij.platform.ide.util.io" />
<orderEntry type="library" name="gson" level="project" />
</component>
</module>

View File

@@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@@ -0,0 +1,185 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"

View File

@@ -0,0 +1,72 @@
message.state.connecting.and.retrieving.options=Connecting to web service and retrieving options...
message.state.downloading.template=Downloading {0} Template...
message.state.preparing.template=Preparing template...
message.whitespaces.are.not.allowed.here=Whitespaces are not allowed here
message.some.parts.are.not.allowed.here=Parts 'con', 'prn', 'aux', 'nul', 'com0', ..., 'com9' and 'lpt0', ..., 'lpt9' are not allowed here
message.field.must.be.set=Field must be set
message.some.string.is.not.a.valid.package.name=''{0}'' is not a valid package name
message.only.latin.characters.digits.spaces.and.some.other.symbols.are.allowed.here=Only latin characters, digits, spaces, '-', '_' and '.' are allowed here
message.only.latin.characters.digits.and.some.other.symbols.are.allowed.here=Only latin characters, digits, '_' and '.' are allowed here
message.must.not.start.or.end.with.dot=Must not start or end with '.'
message.must.not.contain.double.dot.sequences=Must not contain '..' sequences
message.part.is.incorrect.and.must.start.with.latin.character.or.some.other.symbols=Part ''{0}'' is incorrect, it must start with latin character or ''_''
message.only.lowercase.latin.characters.digits.and.some.other.symbols.are.allowed.here=Only lowercase latin characters, digits, '-', '_' and '.' are allowed here
message.must.start.with.lowercase.latin.character=Must start with lowercase latin character
message.allowed.symbols.for.check.artifact.simple.format=Only latin characters, digits, '-' and '_' are allowed here
message.allows.first.symbol.for.check.artifact.simple.format=Must start with latin character or '_'
error.text.with.error.content=Error: {0}
message.unavailable.dependencies=Selected dependencies: {0} are not available for version {1}.
message.no.connection.with.error.content=Initialization failed for ''{0}''\nPlease check URL, network and proxy settings.\n\nError message:\n{1}
message.java.version.not.supported.by.sdk=The selected Java version {0} is not supported by the project SDK ''{1}''.\nThe maximum supported Java version is {2}.
message.title.error=Error
button.tooltip.remove=Remove
button.tooltip.configure=Configure
button.tooltip.retry=Retry
starter.link.guide=Guide
starter.link.reference=Reference
starter.link.website=Web site
starter.link.specification=Specification
starter.generation.error=Error while generating sources for module: {0}
starter.generation.progress=Generating {0} module sources
title.project.server.url.label=Server URL:
title.project.name.label=Name:
title.project.language.label=Language:
title.project.location.label=Location:
title.project.type.label=Type:
title.project.build.system.label=Build system:
title.project.test.framework.label=Test framework:
title.project.group.label=Group:
title.project.artifact.label=Artifact:
title.project.package.label=Package name:
title.project.app.type.label=Application type:
title.project.packaging.label=Packaging:
title.project.sdk.label=Project SDK:
title.project.java.version.label=Java:
title.project.examples.label=Example code
title.project.version.label=Version:
title.project.dependencies.label=Dependencies:
title.project.dependencies.selected.label=Added dependencies:
title.server.url.dialog=Server URL
hint.library.search=Search
hint.no.library.selected=Nothing selected
hint.dependencies.not.selected=No dependencies added
message.specified.path.is.illegal=Specified path is incorrect
message.directory.already.taken.error=Directory is already taken by the project ''{0}''
message.directory.not.writable.error=Directory is not writable
message.file.not.directory.error=Specified path is not a directory
message.directory.not.empty.warning=Directory is not empty
project.settings.warnings.group=Warnings:
project.settings.warnings.ignore=Ignore and continue?

View File

@@ -631,6 +631,4 @@ checkbox.javadocs=Java&docs
checkbox.show.content.of.elements=Show content of elements
label.select.artifact.type=Select artifact &type:
label.cannot.load.artifact=Cannot load artifact
item.name.with.module={0} (module {1})
message.java.version.not.supported.by.sdk=The selected Java version {0} is not supported by the project SDK ''{1}''.\nThe maximum supported Java version is {2}.
message.title.error=Error
item.name.with.module={0} (module {1})

View File

@@ -0,0 +1,21 @@
package com.intellij.ide.starters;
import com.intellij.DynamicBundle;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.PropertyKey;
public final class JavaStartersBundle extends DynamicBundle {
@NonNls private static final String BUNDLE = "messages.JavaStartersBundle";
private static final JavaStartersBundle INSTANCE = new JavaStartersBundle();
private JavaStartersBundle() {
super(BUNDLE);
}
@NotNull
public static @Nls String message(@NotNull @PropertyKey(resourceBundle = BUNDLE) String key, Object @NotNull ... params) {
return INSTANCE.getMessage(key, params);
}
}

View File

@@ -0,0 +1,17 @@
// Copyright 2000-2021 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.
package com.intellij.ide.starters
import com.intellij.openapi.module.Module
interface StarterModuleImporter {
val id: String
val title: String
/**
* Runs import of the module from build system scripts.
*
* @param module Model of created project
* @return false to stop processing
*/
fun runAfterSetup(module: Module): Boolean
}

View File

@@ -0,0 +1,9 @@
// Copyright 2000-2021 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.
package com.intellij.ide.starters
import com.intellij.ide.util.projectWizard.ModuleBuilder
import com.intellij.openapi.module.Module
interface StarterModulePreprocessor {
fun process(module: Module, moduleBuilder: ModuleBuilder, frameworkVersion: String?)
}

View File

@@ -0,0 +1,22 @@
package com.intellij.ide.starters.local
import com.intellij.ide.fileTemplates.FileTemplate
import java.net.URL
sealed class GeneratorAsset {
abstract val targetFileName: String
}
data class GeneratorTemplateFile(
override val targetFileName: String,
val template: FileTemplate
) : GeneratorAsset()
data class GeneratorResourceFile(
override val targetFileName: String,
val resource: URL
) : GeneratorAsset()
data class GeneratorEmptyDirectory(
override val targetFileName: String
) : GeneratorAsset()

View File

@@ -0,0 +1,53 @@
package com.intellij.ide.starters.local
import com.intellij.openapi.vfs.VirtualFile
internal class GeneratorContext(
val starterId: String,
val moduleName: String,
val group: String,
val artifact: String,
val version: String,
val testRunnerId: String?,
private val languageId: String,
private val libraryIds: Set<String>,
private val dependencyConfig: DependencyConfig,
private val properties: Map<String, String>,
val assets: List<GeneratorAsset>,
val outputDirectory: VirtualFile
) {
fun hasLanguage(languageId: String): Boolean {
return this.languageId == languageId
}
fun hasLibrary(libraryId: String): Boolean {
return libraryIds.contains(libraryId)
}
fun hasAnyLibrary(vararg ids: String): Boolean {
return ids.any { libraryIds.contains(it) }
}
fun hasAllLibraries(vararg ids: String): Boolean {
return ids.all { libraryIds.contains(it) }
}
fun getVersion(group: String, artifact: String): String? {
return dependencyConfig.dependencies.find { it.group == group && it.artifact == artifact }?.version
}
fun getBomProperty(propertyId: String): String? {
return dependencyConfig.properties[propertyId]
}
fun getProperty(propertyId: String): String? {
return properties[propertyId]
}
/**
* Renders propertyId as ${propertyId}.
*/
fun asPlaceholder(propertyId: String) : String {
return "\${$propertyId}"
}
}

View File

@@ -0,0 +1,26 @@
package com.intellij.ide.starters.local
import com.intellij.ide.starters.shared.*
import com.intellij.openapi.util.UserDataHolderBase
class StarterContext : UserDataHolderBase() {
var isCreatingNewProject: Boolean = false
lateinit var starterPack: StarterPack
var starter: Starter? = null
var starterDependencyConfig: DependencyConfig? = null
val startersDependencyUpdates: MutableMap<String, DependencyConfig> = mutableMapOf()
var group: String = DEFAULT_MODULE_GROUP
var artifact: String = DEFAULT_MODULE_ARTIFACT
var version: String = DEFAULT_MODULE_VERSION
lateinit var language: StarterLanguage
var projectType: StarterProjectType? = null
var testFramework: StarterTestRunner? = null
var applicationType: StarterAppType? = null
var includeExamples: Boolean = true
val libraryIds: MutableSet<String> = HashSet()
}

View File

@@ -0,0 +1,274 @@
package com.intellij.ide.starters.local
import com.intellij.codeInsight.actions.ReformatCodeProcessor
import com.intellij.ide.starters.JavaStartersBundle
import com.intellij.ide.starters.local.generator.AssetsProcessor
import com.intellij.ide.starters.local.wizard.StarterInitialStep
import com.intellij.ide.starters.local.wizard.StarterLibrariesStep
import com.intellij.ide.starters.shared.*
import com.intellij.ide.projectWizard.ProjectSettingsStep
import com.intellij.ide.starters.StarterModuleImporter
import com.intellij.ide.starters.StarterModulePreprocessor
import com.intellij.ide.util.projectWizard.*
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.application.WriteAction
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.openapi.externalSystem.model.ExternalSystemDataKeys
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.fileEditor.OpenFileDescriptor
import com.intellij.openapi.module.JavaModuleType
import com.intellij.openapi.module.Module
import com.intellij.openapi.module.ModuleType
import com.intellij.openapi.module.StdModuleTypes
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.project.rootManager
import com.intellij.openapi.projectRoots.JavaSdkType
import com.intellij.openapi.projectRoots.SdkTypeId
import com.intellij.openapi.roots.ModifiableRootModel
import com.intellij.openapi.roots.ProjectRootManager
import com.intellij.openapi.roots.ui.configuration.ModulesProvider
import com.intellij.openapi.startup.StartupManager
import com.intellij.openapi.ui.Messages
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.pom.java.LanguageLevel
import com.intellij.ui.GuiUtils
import com.intellij.util.lang.JavaVersion
import java.io.IOException
import java.net.URL
abstract class StarterModuleBuilder : ModuleBuilder() {
companion object {
@JvmStatic
private val VALID_PACKAGE_NAME_PATTERN: Regex = Regex("[^a-zA-Z0-9_.]")
@JvmStatic
private val IMPORTER_EP_NAME: ExtensionPointName<StarterModuleImporter> =
ExtensionPointName.create("com.intellij.starter.moduleImporter")
@JvmStatic
private val PREPROCESSOR_EP_NAME: ExtensionPointName<StarterModulePreprocessor> =
ExtensionPointName.create("com.intellij.starter.modulePreprocessor")
@JvmStatic
fun getPackageName(group: String, artifact: String): String {
val artifactSanitized = artifact.replace(VALID_PACKAGE_NAME_PATTERN, "_")
val groupSanitized = group.replace("-", ".").replace(VALID_PACKAGE_NAME_PATTERN, "_")
return "$groupSanitized.$artifactSanitized"
}
fun importModule(module: Module) {
if (module.isDisposed) return
val moduleBuilderPostTasks = IMPORTER_EP_NAME.extensions
for (task in moduleBuilderPostTasks) {
if (!task.runAfterSetup(module)) break
}
}
fun preprocessModule(module: Module, builder: ModuleBuilder, frameworkVersion: String?) {
PREPROCESSOR_EP_NAME.forEachExtensionSafe {
it.process(module, builder, frameworkVersion)
}
}
internal fun openSampleFiles(module: Module, filePathsToOpen: List<String>) {
val contentRoot = module.rootManager.contentRoots.firstOrNull()
if (contentRoot != null) {
val fileEditorManager = FileEditorManager.getInstance(module.project)
for (filePath in filePathsToOpen) {
val fileToOpen = VfsUtil.findRelativeFile(filePath, contentRoot)
if (fileToOpen != null) {
fileEditorManager.openTextEditor(OpenFileDescriptor(module.project, fileToOpen), true)
}
else {
logger<StarterModuleBuilder>().debug("Unable to find sample file $filePath in module: ${module.name}")
}
}
}
}
}
protected val starterContext: StarterContext = StarterContext()
private val starterSettings: StarterWizardSettings by lazy { createSettings() }
override fun getModuleType(): ModuleType<*> = StdModuleTypes.JAVA
override fun getParentGroup(): String = JavaModuleType.BUILD_TOOLS_GROUP
override fun getWeight(): Int = JavaModuleBuilder.BUILD_SYSTEM_WEIGHT + 10
open fun getHelpId(): String? = null
protected abstract fun getProjectTypes() : List<StarterProjectType>
protected abstract fun getLanguages(): List<StarterLanguage>
protected abstract fun getStarterPack(): StarterPack
protected abstract fun getTestFrameworks(): List<StarterTestRunner>
protected abstract fun getAssets(starter: Starter): List<GeneratorAsset>
protected open fun isExampleCodeProvided(): Boolean = false
protected open fun getMinJavaVersion() : JavaVersion? = LanguageLevel.JDK_1_8.toJavaVersion()
protected open fun getCustomizedMessages(): CustomizedMessages? = null
protected open fun getFilePathsToOpen(): List<String> = emptyList()
internal fun isDependencyAvailableInternal(starter: Starter, dependency: Library): Boolean {
return isDependencyAvailable(starter, dependency)
}
protected open fun isDependencyAvailable(starter: Starter, dependency: Library): Boolean {
return true
}
override fun isSuitableSdkType(sdkType: SdkTypeId?): Boolean {
return sdkType is JavaSdkType && !sdkType.isDependent
}
override fun modifyProjectTypeStep(settingsStep: SettingsStep): ModuleWizardStep? {
// do not add standard SDK selector at the top
return null
}
override fun setupModule(module: Module) {
super.setupModule(module)
if (starterContext.isCreatingNewProject) {
val project = module.project
project.putUserData(ExternalSystemDataKeys.NEWLY_CREATED_PROJECT, java.lang.Boolean.TRUE)
project.putUserData(ExternalSystemDataKeys.NEWLY_IMPORTED_PROJECT, java.lang.Boolean.TRUE)
}
startGenerator(module)
}
private fun createSettings() : StarterWizardSettings {
return StarterWizardSettings(
getProjectTypes(),
getLanguages(),
isExampleCodeProvided(),
false,
emptyList(),
null,
emptyList(),
emptyList(),
getTestFrameworks(),
getCustomizedMessages()
)
}
override fun getCustomOptionsStep(context: WizardContext, parentDisposable: Disposable): ModuleWizardStep {
starterContext.language = starterSettings.languages.first()
starterContext.testFramework = starterSettings.testFrameworks.firstOrNull()
starterContext.projectType = starterSettings.projectTypes.firstOrNull()
starterContext.applicationType = starterSettings.applicationTypes.firstOrNull()
starterContext.isCreatingNewProject = context.isCreatingNewProject
return createOptionsStep(StarterContextProvider(this, parentDisposable, starterContext, context, starterSettings, ::getStarterPack))
}
override fun createWizardSteps(context: WizardContext, modulesProvider: ModulesProvider): Array<ModuleWizardStep> {
return arrayOf(createLibrariesStep(StarterContextProvider(this, context.disposable, starterContext, context, starterSettings, ::getStarterPack)))
}
protected open fun createOptionsStep(contextProvider: StarterContextProvider): StarterInitialStep {
return StarterInitialStep(contextProvider)
}
protected open fun createLibrariesStep(contextProvider: StarterContextProvider): StarterLibrariesStep {
return StarterLibrariesStep(contextProvider)
}
override fun getIgnoredSteps(): List<Class<out ModuleWizardStep>> {
return listOf(ProjectSettingsStep::class.java)
}
internal fun getMinJavaVersionInternal() : JavaVersion? = getMinJavaVersion()
override fun setupRootModel(modifiableRootModel: ModifiableRootModel) {
val sdk = if (moduleJdk != null) moduleJdk else ProjectRootManager.getInstance(modifiableRootModel.project).projectSdk
if (sdk != null) {
modifiableRootModel.sdk = sdk
}
doAddContentEntry(modifiableRootModel)
}
private fun startGenerator(module: Module) {
val moduleContentRoot = LocalFileSystem.getInstance().refreshAndFindFileByPath(contentEntryPath!!.replace("\\", "/"))
?: throw IllegalStateException("Module root not found")
val starter = starterContext.starter ?: throw IllegalStateException("Starter is not set")
val generatorContext = GeneratorContext(
starter.id,
module.name,
starterContext.group,
starterContext.artifact,
starterContext.version,
starterContext.testFramework!!.id,
starterContext.language.id,
starterContext.libraryIds,
starterContext.starterDependencyConfig ?: error("Starter dependency config is not set"),
getGeneratorContextProperties(),
getAssets(starter),
moduleContentRoot
)
ProgressManager.getInstance().runProcessWithProgressSynchronously(
{
WriteAction.runAndWait<Throwable> {
try {
AssetsProcessor().generateSources(generatorContext, getTemplateProperties())
}
catch (e: IOException) {
logger<StarterModuleBuilder>().error("Unable to create module by template", e)
ApplicationManager.getApplication().invokeLater {
Messages.showErrorDialog(
JavaStartersBundle.message("starter.generation.error", e.message ?: ""),
presentableName)
}
}
applyAdditionalChanges(module)
}
},
JavaStartersBundle.message("starter.generation.progress", presentableName),
true, module.project)
StartupManager.getInstance(module.project).runAfterOpened { // IDEA-244863
GuiUtils.invokeLaterIfNeeded(Runnable {
if (module.isDisposed) return@Runnable
ReformatCodeProcessor(module.project, module, false).run()
// import of module may dispose it and create another, open files first
openSampleFiles(module, getFilePathsToOpen())
importModule(module)
}, ModalityState.NON_MODAL, module.disposed)
}
}
open fun getTemplateProperties(): Map<String, Any> = emptyMap()
open fun applyAdditionalChanges(module: Module) {
// optional hook method
}
protected fun getDependencyConfig(resourcePath: String): URL {
return javaClass.getResource(resourcePath) ?: error("Failed to get resource: $resourcePath")
}
protected open fun getGeneratorContextProperties(): Map<String, String> {
val packageName = getPackageName(starterContext.group, starterContext.artifact)
return mapOf("rootPackage" to packageName)
}
protected fun getSamplesExt(language: StarterLanguage): String {
return when (language.id) {
"java" -> "java"
"groovy" -> "groovy"
"kotlin" -> "kt"
else -> throw UnsupportedOperationException()
}
}
}

View File

@@ -0,0 +1,119 @@
@file:JvmName("StarterUtils")
package com.intellij.ide.starters.local
import com.intellij.openapi.util.Version
import org.jdom.Element
import java.io.File
import java.io.IOException
import java.nio.file.Files
import java.nio.file.attribute.BasicFileAttributes
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneId
internal object StarterUtils {
private val PLACEHOLDER_VERSION_PATTERN: Regex = Regex("\\$\\{(.+)}")
private class IncorrectBomFileException(message: String) : IOException(message)
internal fun parseDependencyConfig(projectTag: Element, resourcePath: String, interpolateProperties: Boolean = true): DependencyConfig {
val properties: MutableMap<String, String> = mutableMapOf()
val dependencies: MutableList<Dependency> = mutableListOf()
val bomVersion: String
if (projectTag.name != "project") throw IncorrectBomFileException("Incorrect root tag name ${projectTag.name}")
val bomVersionText = projectTag.getChild("version")?.text
if (bomVersionText.isNullOrEmpty()) throw IncorrectBomFileException("Empty BOM version for ${resourcePath}")
bomVersion = bomVersionText
val propertiesTag = projectTag.getChild("properties")
if (propertiesTag != null) {
for (propertyTag in propertiesTag.children) {
val propertyName = propertyTag.name
val propertyValue = propertyTag.text
if (propertyName == null || propertyValue.isNullOrBlank()) {
throw IncorrectBomFileException("Incorrect property '${propertyName}'")
}
properties[propertyName] = propertyValue
}
}
val dependenciesTag = projectTag.getChild("dependencyManagement")?.getChild("dependencies")
if (dependenciesTag != null) {
for (dependencyTag in dependenciesTag.getChildren("dependency")) {
val groupId = dependencyTag.getChild("groupId")?.text
val artifactId = dependencyTag.getChild("artifactId")?.text
var version = dependencyTag.getChild("version")?.text
if (groupId.isNullOrEmpty() || artifactId.isNullOrEmpty() || version.isNullOrEmpty()) {
throw IncorrectBomFileException("Incorrect dependency '${groupId}:${artifactId}'")
}
version = interpolateDependencyVersion(groupId, artifactId, version, properties, interpolateProperties)
dependencies.add(Dependency(groupId, artifactId, version))
}
}
return DependencyConfig(bomVersion, properties, dependencies)
}
internal fun parseDependencyConfigVersion(projectTag: Element, resourcePath: String): Version {
if (projectTag.name != "project") throw IncorrectBomFileException("Incorrect root tag name ${projectTag.name}")
val bomVersionText = projectTag.getChild("version")?.text
if (bomVersionText.isNullOrEmpty()) throw IncorrectBomFileException("Empty BOM version for ${resourcePath}")
return Version.parseVersion(bomVersionText) ?: error("Failed to parse starter dependency config version")
}
internal fun mergeDependencyConfigs(dependencyConfig: DependencyConfig, dependencyConfigUpdates: DependencyConfig?): DependencyConfig {
if (dependencyConfigUpdates == null ||
dependencyConfig.version.toFloat() > dependencyConfigUpdates.version.toFloat()) return dependencyConfig
val newVersion = dependencyConfigUpdates.version
val properties = (dependencyConfig.properties.keys union dependencyConfigUpdates.properties.keys).associateWith { propertyKey ->
dependencyConfigUpdates.properties[propertyKey] ?: dependencyConfig.properties[propertyKey]
?: error("Failed to find property value for key: $propertyKey")
}
val dependencies = dependencyConfig.dependencies.map { dependency ->
val newDependencyVersion = (dependencyConfigUpdates.dependencies.find { updatedDependency ->
dependency.group == updatedDependency.group && dependency.artifact == updatedDependency.artifact
}?.version ?: dependency.version).let {
interpolateDependencyVersion(dependency.group, dependency.artifact, it, properties, true)
}
Dependency(dependency.group, dependency.artifact, newDependencyVersion)
}
return DependencyConfig(newVersion, properties, dependencies)
}
internal fun isDependencyUpdateFileExpired(file: File): Boolean {
val lastModifiedMs = Files.readAttributes(file.toPath(), BasicFileAttributes::class.java).lastModifiedTime().toMillis()
val lastModified = Instant.ofEpochMilli(lastModifiedMs).atZone(ZoneId.systemDefault()).toLocalDateTime()
return lastModified.isBefore(LocalDate.now().atStartOfDay())
}
private fun interpolateDependencyVersion(groupId: String, artifactId: String, version: String,
properties: Map<String, String>, interpolateProperties: Boolean = true): String {
val versionMatch = PLACEHOLDER_VERSION_PATTERN.matchEntire(version)
if (versionMatch != null) {
val propertyName = versionMatch.groupValues[1]
val propertyValue = properties[propertyName]
if (propertyValue.isNullOrEmpty()) {
throw IncorrectBomFileException("No such property '${propertyName}' for version of '${groupId}:${artifactId}'")
}
return if (interpolateProperties) propertyValue else version
}
return version
}
}

View File

@@ -0,0 +1,74 @@
package com.intellij.ide.starters.local
import com.intellij.ide.starters.shared.LibraryInfo
import com.intellij.ide.starters.shared.LibraryLink
import com.intellij.ide.starters.shared.StarterWizardSettings
import com.intellij.ide.util.projectWizard.WizardContext
import com.intellij.openapi.Disposable
import com.intellij.openapi.util.NlsSafe
import org.jetbrains.annotations.Nls
import java.net.URL
import javax.swing.Icon
data class Dependency(
val group: String,
val artifact: String,
val version: String
)
data class DependencyConfig(
val version: String,
val properties: Map<String, String>,
val dependencies: List<Dependency>
) {
fun getVersion(group: String, artifact: String): String? {
return dependencies.find { it.group == group && it.artifact == artifact }?.version
}
}
data class LibraryCategory(
val icon: Icon?,
@Nls val title: String,
@Nls val description: String
)
class Library (
val id: String,
val icon: Icon?,
@NlsSafe
override val title: String,
@Nls
override val description: String,
val group: String?,
val artifact: String?,
override val links: List<LibraryLink> = emptyList(),
val category: LibraryCategory? = null,
override val isRequired: Boolean = false,
override val isDefault: Boolean = false,
val includesLibraries: Set<String> = emptySet()
) : LibraryInfo {
override fun toString(): String {
return "Library($id)"
}
}
data class Starter(
val id: String,
val title: String,
val versionConfigUrl: URL,
val libraries: List<Library>
)
data class StarterPack(
val defaultStarterId: String,
val starters: List<Starter>
)
class StarterContextProvider(
val moduleBuilder: StarterModuleBuilder,
val parentDisposable: Disposable,
val starterContext: StarterContext,
val wizardContext: WizardContext,
val settings: StarterWizardSettings,
val starterPackProvider: () -> StarterPack
)

View File

@@ -0,0 +1,71 @@
package com.intellij.ide.starters.local.generator
import com.intellij.ide.starters.local.GeneratorContext
import com.intellij.ide.starters.local.GeneratorEmptyDirectory
import com.intellij.ide.starters.local.GeneratorResourceFile
import com.intellij.ide.starters.local.GeneratorTemplateFile
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.vfs.VfsUtil
import java.io.IOException
internal class AssetsProcessor {
fun generateSources(context: GeneratorContext, templateProps: Map<String, Any>) {
val templateProperties = mutableMapOf<String, Any>()
templateProperties["context"] = context
templateProperties.putAll(templateProps)
val log = logger<AssetsProcessor>()
for (asset in context.assets) {
val subPath = if (asset.targetFileName.contains("/"))
"/" + asset.targetFileName.substringBeforeLast("/")
else
""
val outputDirectory = VfsUtil.createDirectoryIfMissing(context.outputDirectory.path)
?: throw IllegalStateException("Unable to create directory ${context.outputDirectory.path}")
if (asset is GeneratorEmptyDirectory) {
log.info("Creating empty directory ${asset.targetFileName} in ${outputDirectory.path}")
VfsUtil.createDirectoryIfMissing(outputDirectory, asset.targetFileName)
}
else {
val fileDirectory = if (subPath.isEmpty()) {
outputDirectory
}
else {
VfsUtil.createDirectoryIfMissing(outputDirectory, subPath)
?: throw IllegalStateException("Unable to create directory ${subPath} in ${context.outputDirectory.path}")
}
val fileName = asset.targetFileName.substringAfterLast("/")
log.info("Creating file $fileName in ${fileDirectory.path}")
when (asset) {
is GeneratorTemplateFile -> {
val sourceCode: String
try {
sourceCode = asset.template.getText(templateProperties)
}
catch (e: Exception) {
throw TemplateProcessingException(e)
}
val file = fileDirectory.findOrCreateChildData(this, fileName)
VfsUtil.saveText(file, sourceCode)
}
is GeneratorResourceFile -> {
val file = fileDirectory.findOrCreateChildData(this, fileName)
asset.resource.openStream().use {
file.setBinaryContent(it.readBytes())
}
}
else -> {
throw UnsupportedOperationException("Unsupported asset type")
}
}
}
}
}
private class TemplateProcessingException(t: Throwable) : IOException("Unable to process template", t)
}

View File

@@ -0,0 +1,14 @@
package com.intellij.ide.starters.local.gradle
import com.intellij.ide.starters.local.GeneratorAsset
import com.intellij.ide.starters.local.GeneratorResourceFile
class GradleResourcesProvider {
fun getGradlewResources(): List<GeneratorAsset> {
return listOf(
GeneratorResourceFile("gradle/wrapper/gradle-wrapper.jar", javaClass.getResource("/assets/gradlew/gradle-wrapper.jar.bin")!!),
GeneratorResourceFile("gradlew", javaClass.getResource("/assets/gradlew/gradlew.bin")!!),
GeneratorResourceFile("gradlew.bat", javaClass.getResource("/assets/gradlew/gradlew.bat.bin")!!)
)
}
}

View File

@@ -0,0 +1,335 @@
package com.intellij.ide.starters.local.wizard
import com.intellij.ide.starters.JavaStartersBundle
import com.intellij.ide.starters.local.*
import com.intellij.ide.starters.shared.*
import com.intellij.ide.starters.shared.ValidationFunctions.*
import com.intellij.ide.util.projectWizard.ModuleWizardStep
import com.intellij.ide.util.projectWizard.WizardContext
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.PathManager
import com.intellij.openapi.application.impl.ApplicationInfoImpl
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.observable.properties.GraphProperty
import com.intellij.openapi.observable.properties.GraphPropertyImpl.Companion.graphProperty
import com.intellij.openapi.observable.properties.PropertyGraph
import com.intellij.openapi.observable.properties.map
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.roots.ui.configuration.sdkComboBox
import com.intellij.openapi.roots.ui.configuration.projectRoot.ProjectSdksModel
import com.intellij.openapi.roots.ui.configuration.validateJavaVersion
import com.intellij.openapi.roots.ui.configuration.validateSdk
import com.intellij.openapi.ui.DialogPanel
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.JDOMUtil
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.util.registry.Registry
import com.intellij.ui.SimpleListCellRenderer
import com.intellij.ui.layout.*
import com.intellij.util.concurrency.AppExecutorUtil
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import com.intellij.util.io.HttpRequests
import com.intellij.util.text.nullize
import com.intellij.util.ui.UIUtil.invokeLaterIfNeeded
import org.jdom.Element
import java.io.File
import java.nio.file.Files
import javax.swing.DefaultComboBoxModel
import javax.swing.JComponent
import javax.swing.JTextField
open class StarterInitialStep(contextProvider: StarterContextProvider) : ModuleWizardStep() {
protected val moduleBuilder: StarterModuleBuilder = contextProvider.moduleBuilder
protected val wizardContext: WizardContext = contextProvider.wizardContext
protected val starterContext: StarterContext = contextProvider.starterContext
protected val starterSettings: StarterWizardSettings = contextProvider.settings
protected val parentDisposable: Disposable = contextProvider.parentDisposable
private val starterPackProvider: () -> StarterPack = contextProvider.starterPackProvider
private val validatedTextComponents: MutableList<JTextField> = mutableListOf()
protected val propertyGraph: PropertyGraph = PropertyGraph()
private val entityNameProperty: GraphProperty<String> = propertyGraph.graphProperty(::suggestName)
private val locationProperty: GraphProperty<String> = propertyGraph.graphProperty(::suggestLocationByName)
private val groupIdProperty: GraphProperty<String> = propertyGraph.graphProperty { starterContext.group }
private val artifactIdProperty: GraphProperty<String> = propertyGraph.graphProperty { entityName }
private val sdkProperty: GraphProperty<Sdk?> = propertyGraph.graphProperty { null }
private val projectTypeProperty: GraphProperty<StarterProjectType?> = propertyGraph.graphProperty { starterContext.projectType }
private val languageProperty: GraphProperty<StarterLanguage> = propertyGraph.graphProperty { starterContext.language }
private val testFrameworkProperty: GraphProperty<StarterTestRunner?> = propertyGraph.graphProperty { starterContext.testFramework }
private val applicationTypeProperty: GraphProperty<StarterAppType?> = propertyGraph.graphProperty { starterContext.applicationType }
private val exampleCodeProperty: GraphProperty<Boolean> = propertyGraph.graphProperty { starterContext.includeExamples }
private var entityName: String by entityNameProperty.map { it.trim() }
private var location: String by locationProperty
private var groupId: String by groupIdProperty.map { it.trim() }
private var artifactId: String by artifactIdProperty.map { it.trim() }
private val contentPanel: DialogPanel by lazy { createComponent() }
private val sdkModel: ProjectSdksModel = ProjectSdksModel()
override fun getHelpId(): String? = moduleBuilder.getHelpId()
init {
Disposer.register(parentDisposable, Disposable {
sdkModel.disposeUIResources()
})
}
override fun updateDataModel() {
starterContext.projectType = projectTypeProperty.get()
starterContext.language = languageProperty.get()
starterContext.group = groupId
starterContext.artifact = artifactId
starterContext.testFramework = testFrameworkProperty.get()
starterContext.includeExamples = exampleCodeProperty.get()
wizardContext.projectName = entityName
wizardContext.setProjectFileDirectory(location)
val sdk = sdkProperty.get()
if (wizardContext.project == null) {
wizardContext.projectJdk = sdk
}
else {
moduleBuilder.moduleJdk = sdk
}
}
override fun getComponent(): JComponent {
return contentPanel
}
private fun createComponent(): DialogPanel {
entityNameProperty.dependsOn(locationProperty) { File(location).name }
entityNameProperty.dependsOn(artifactIdProperty) { artifactId }
locationProperty.dependsOn(entityNameProperty, ::suggestLocationByName)
artifactIdProperty.dependsOn(entityNameProperty) { entityName }
// query dependencies from builder, called only once
val starterPack = starterPackProvider.invoke()
starterContext.starterPack = starterPack
updateStartersDependencies(starterPack)
return panel {
row(JavaStartersBundle.message("title.project.name.label")) {
textField(entityNameProperty)
.growPolicy(GrowPolicy.SHORT_TEXT)
.withSpecialValidation(CHECK_NOT_EMPTY, CHECK_SIMPLE_NAME_FORMAT)
.focused()
}.largeGapAfter()
row(JavaStartersBundle.message("title.project.location.label")) {
projectLocationField(locationProperty, wizardContext)
.withSpecialValidation(listOf(CHECK_NOT_EMPTY, CHECK_LOCATION_FOR_ERROR), CHECK_LOCATION_FOR_WARNING)
}.largeGapAfter()
addFieldsBefore(this)
if (starterSettings.applicationTypes.isNotEmpty()) {
row(JavaStartersBundle.message("title.project.app.type.label")) {
val applicationTypesModel = DefaultComboBoxModel<StarterAppType>()
applicationTypesModel.addAll(starterSettings.applicationTypes)
comboBox(applicationTypesModel, applicationTypeProperty, SimpleListCellRenderer.create("") { it?.title ?: "" })
.growPolicy(GrowPolicy.SHORT_TEXT)
}.largeGapAfter()
}
if (starterSettings.languages.size > 1) {
row(JavaStartersBundle.message("title.project.language.label")) {
buttonSelector(starterSettings.languages, languageProperty) { it.title }
}.largeGapAfter()
}
if (starterSettings.projectTypes.isNotEmpty()) {
val messages = starterSettings.customizedMessages
row(messages?.projectTypeLabel ?: JavaStartersBundle.message("title.project.build.system.label")) {
buttonSelector(starterSettings.projectTypes, projectTypeProperty) { it?.title ?: "" }
}.largeGapAfter()
}
if (starterSettings.testFrameworks.isNotEmpty()) {
row(JavaStartersBundle.message("title.project.test.framework.label")) {
buttonSelector(starterSettings.testFrameworks, testFrameworkProperty) { it?.title ?: "" }
}.largeGapAfter()
}
row(JavaStartersBundle.message("title.project.group.label")) {
textField(groupIdProperty)
.growPolicy(GrowPolicy.SHORT_TEXT)
.withSpecialValidation(CHECK_NOT_EMPTY, CHECK_NO_WHITESPACES, CHECK_GROUP_FORMAT, CHECK_NO_RESERVED_WORDS)
}.largeGapAfter()
row(JavaStartersBundle.message("title.project.artifact.label")) {
textField(artifactIdProperty)
.growPolicy(GrowPolicy.SHORT_TEXT)
.withSpecialValidation(CHECK_NOT_EMPTY, CHECK_NO_WHITESPACES, CHECK_ARTIFACT_SIMPLE_FORMAT, CHECK_NO_RESERVED_WORDS)
}.largeGapAfter()
row(JavaStartersBundle.message("title.project.sdk.label")) {
sdkComboBox(sdkModel, sdkProperty, wizardContext.project, moduleBuilder)
.growPolicy(GrowPolicy.SHORT_TEXT)
}.largeGapAfter()
if (starterSettings.isExampleCodeProvided) {
row {
checkBox(JavaStartersBundle.message("title.project.examples.label"), exampleCodeProperty)
}
}
addFieldsAfter(this)
}.withVisualPadding(NAME_FIELD_TOP_PADDING)
}
protected open fun addFieldsBefore(layout: LayoutBuilder) {}
protected open fun addFieldsAfter(layout: LayoutBuilder) {}
override fun validate(): Boolean {
if (!validateFormFields(component, contentPanel, validatedTextComponents)) {
return false
}
if (!validateSdk(sdkProperty, sdkModel)) {
return false
}
if (!validateJavaVersion(sdkProperty, moduleBuilder.getMinJavaVersionInternal()?.toFeatureString())) {
return false
}
return true
}
private fun updateStartersDependencies(starterPack: StarterPack) {
val starters = starterPack.starters
AppExecutorUtil.getAppExecutorService().submit {
checkDependencyUpdates(starters)
}
}
@RequiresBackgroundThread
private fun checkDependencyUpdates(starters: List<Starter>) {
for (starter in starters) {
val localUpdates = loadStarterDependencyUpdatesFromFile(starter.id)
if (localUpdates != null) {
setStarterDependencyUpdates(starter.id, localUpdates)
return
}
val externalUpdates = loadStarterDependencyUpdatesFromNetwork(starter.id) ?: return
val (dependencyUpdates, resourcePath) = externalUpdates
if (isDisposed()) return
val dependencyConfig = StarterUtils.parseDependencyConfig(dependencyUpdates, resourcePath)
if (isDisposed()) return
saveStarterDependencyUpdatesToFile(starter.id, dependencyUpdates)
setStarterDependencyUpdates(starter.id, dependencyConfig)
}
}
private fun isDisposed(): Boolean {
return Disposer.isDisposed(parentDisposable)
}
private fun suggestName(): String {
return suggestName(starterContext.artifact)
}
private fun suggestName(prefix: String): String {
val projectFileDirectory = File(wizardContext.projectFileDirectory)
return FileUtil.createSequentFileName(projectFileDirectory, prefix, "")
}
private fun suggestLocationByName(): String {
return FileUtil.join(wizardContext.projectFileDirectory, entityName)
}
@RequiresBackgroundThread
private fun loadStarterDependencyUpdatesFromFile(starterId: String): DependencyConfig? {
val configUpdateDir = File(PathManager.getTempPath(), getDependencyConfigUpdatesDirLocation(starterId))
val configUpdateFile = File(configUpdateDir, getPatchFileName(starterId))
if (!configUpdateFile.exists()
|| StarterUtils.isDependencyUpdateFileExpired(configUpdateFile)) {
return null
}
val resourcePath = configUpdateFile.absolutePath
return try {
StarterUtils.parseDependencyConfig(JDOMUtil.load(configUpdateFile), resourcePath)
}
catch (e: Exception) {
logger<StarterInitialStep>().warn("Failed to load starter dependency updates from file: $resourcePath. The file will be deleted.")
FileUtil.delete(configUpdateFile)
null
}
}
@RequiresBackgroundThread
private fun loadStarterDependencyUpdatesFromNetwork(starterId: String): Pair<Element, String>? {
val url = buildStarterPatchUrl(starterId) ?: return null
return try {
val content = HttpRequests.request(url)
.accept("application/xml")
.readString()
return JDOMUtil.load(content) to url
}
catch (e: Exception) {
if (e is HttpRequests.HttpStatusException
&& (e.statusCode == 403 || e.statusCode == 404)) {
logger<StarterInitialStep>().debug("No updates for $starterId: $url")
}
else {
logger<StarterInitialStep>().warn("Unable to load external starter $starterId dependency updates from: $url", e)
}
null
}
}
@RequiresBackgroundThread
private fun saveStarterDependencyUpdatesToFile(starterId: String, dependencyConfigUpdate: Element) {
val configUpdateDir = File(PathManager.getTempPath(), getDependencyConfigUpdatesDirLocation(starterId))
if (!configUpdateDir.exists()) {
Files.createDirectories(configUpdateDir.toPath())
}
val configUpdateFile = File(configUpdateDir, getPatchFileName(starterId))
JDOMUtil.write(dependencyConfigUpdate, configUpdateFile)
}
private fun setStarterDependencyUpdates(starterId: String, dependencyConfigUpdate: DependencyConfig) {
invokeLaterIfNeeded {
if (isDisposed()) return@invokeLaterIfNeeded
starterContext.startersDependencyUpdates[starterId] = dependencyConfigUpdate
}
}
private fun buildStarterPatchUrl(starterId: String): String? {
val host = Registry.stringValue("starters.dependency.update.host").nullize(true) ?: return null
val ideVersion = ApplicationInfoImpl.getShadowInstance().let { "${it.majorVersion}.${it.minorVersion}" }
val patchFileName = getPatchFileName(starterId)
return "$host/starter/$starterId/$ideVersion/$patchFileName"
}
private fun getDependencyConfigUpdatesDirLocation(starterId: String): String = "framework-starters/$starterId/"
private fun getPatchFileName(starterId: String): String = "${starterId}_patch.pom"
@Suppress("SameParameterValue")
private fun <T : JComponent> CellBuilder<T>.withSpecialValidation(vararg errorValidationUnits: TextValidationFunction): CellBuilder<T> =
withValidation(this, errorValidationUnits.asList(), null, validatedTextComponents, parentDisposable)
private fun <T : JComponent> CellBuilder<T>.withSpecialValidation(
errorValidationUnits: List<TextValidationFunction>,
warningValidationUnit: TextValidationFunction?
): CellBuilder<T> {
return withValidation(this, errorValidationUnits, warningValidationUnit, validatedTextComponents, parentDisposable)
}
}

View File

@@ -0,0 +1,393 @@
package com.intellij.ide.starters.local.wizard
import com.intellij.ide.starters.JavaStartersBundle
import com.intellij.ide.starters.local.*
import com.intellij.ide.starters.shared.*
import com.intellij.ide.starters.shared.ui.LibraryDescriptionPanel
import com.intellij.ide.starters.shared.ui.SelectedLibrariesPanel
import com.intellij.icons.AllIcons
import com.intellij.ide.util.projectWizard.ModuleWizardStep
import com.intellij.openapi.ui.ComboBox
import com.intellij.openapi.ui.DialogPanel
import com.intellij.openapi.util.JDOMUtil
import com.intellij.openapi.util.NlsSafe
import com.intellij.openapi.util.Version
import com.intellij.ui.*
import com.intellij.ui.components.JBLabel
import com.intellij.ui.layout.*
import com.intellij.util.containers.Convertor
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.UIUtil
import com.intellij.util.ui.components.BorderLayoutPanel
import com.intellij.util.ui.tree.TreeUtil
import java.awt.Dimension
import java.awt.GridBagLayout
import java.awt.event.ItemEvent
import javax.swing.DefaultComboBoxModel
import javax.swing.JComponent
import javax.swing.JPanel
import javax.swing.JTree
import javax.swing.event.TreeSelectionListener
import javax.swing.tree.DefaultMutableTreeNode
import javax.swing.tree.DefaultTreeModel
import javax.swing.tree.TreePath
import javax.swing.tree.TreeSelectionModel
class StarterLibrariesStep(contextProvider: StarterContextProvider) : ModuleWizardStep() {
protected val starterContext = contextProvider.starterContext
protected val starterSettings: StarterWizardSettings = contextProvider.settings
protected val moduleBuilder: StarterModuleBuilder = contextProvider.moduleBuilder
private val topLevelPanel: BorderLayoutPanel = BorderLayoutPanel()
private val contentPanel: DialogPanel by lazy { createComponent() }
private val libraryDescriptionPanel: LibraryDescriptionPanel by lazy { LibraryDescriptionPanel() }
private val selectedLibrariesPanel: SelectedLibrariesPanel by lazy { createSelectedLibrariesPanel() }
private val dependencyConfig: Map<String, DependencyConfig> by lazy { loadDependencyConfig() }
private val selectedLibraryIds: MutableSet<String> = mutableSetOf()
private var selectedStarterId: String? = null
private val startersComboBox: ComboBox<Starter> by lazy { ComboBox<Starter>() }
private val librariesList: CheckboxTreeBase by lazy { createLibrariesList() }
override fun updateDataModel() {
starterContext.starter = startersComboBox.selectedItem as? Starter
starterContext.libraryIds.clear()
starterContext.libraryIds.addAll(selectedLibraryIds)
starterContext.starterDependencyConfig = dependencyConfig[starterContext.starter?.id]
}
override fun onStepLeaving() {
super.onStepLeaving()
updateDataModel()
}
override fun getComponent(): JComponent {
return topLevelPanel
}
private fun loadDependencyConfig(): Map<String, DependencyConfig> {
return starterContext.starterPack.starters.associate { starter ->
starter.id to starter.versionConfigUrl.openStream().use {
val dependencyConfigUpdates = starterContext.startersDependencyUpdates[starter.id]
val dependencyConfigUpdatesVersion = dependencyConfigUpdates?.version?.let { version -> Version.parseVersion(version) }
?: Version(-1, -1, -1)
val starterDependencyConfig = JDOMUtil.load(it)
val starterDependencyConfigVersion = StarterUtils.parseDependencyConfigVersion(starterDependencyConfig,
starter.versionConfigUrl.path)
val mergeDependencyUpdate = starterDependencyConfigVersion < dependencyConfigUpdatesVersion
if (mergeDependencyUpdate) {
StarterUtils.mergeDependencyConfigs(
StarterUtils.parseDependencyConfig(starterDependencyConfig, starter.versionConfigUrl.path, false),
dependencyConfigUpdates)
}
else {
StarterUtils.parseDependencyConfig(starterDependencyConfig, starter.versionConfigUrl.path)
}
}
}
}
@NlsSafe
private fun getLibraryVersion(library: Library): String? {
if (library.group == null || library.artifact == null) return null
val selectedStarter = startersComboBox.selectedItem as? Starter ?: return null
return dependencyConfig[selectedStarter.id]?.getVersion(library.group, library.artifact)
}
override fun getPreferredFocusedComponent(): JComponent {
return librariesList
}
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)
val renderer = textRenderer
when (val item = value.userObject) {
is LibraryCategory -> {
renderer.icon = item.icon ?: AllIcons.Nodes.PpLibFolder
renderer.append(item.title, SimpleTextAttributes.REGULAR_ATTRIBUTES)
}
is Library -> {
renderer.icon = item.icon ?: AllIcons.Nodes.PpLib
renderer.append(item.title, SimpleTextAttributes.REGULAR_ATTRIBUTES)
val version = getLibraryVersion(item)
if (version != null) {
renderer.append(" ($version)", SimpleTextAttributes.GRAYED_ATTRIBUTES)
}
}
}
}
}, null)
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 library = node.userObject as? Library ?: return
val libraryId = library.id
if (node.isChecked) {
selectedLibraryIds.add(libraryId)
selectedLibraryIds.removeAll(library.includesLibraries)
}
else {
selectedLibraryIds.remove(libraryId)
}
updateIncludedLibraries(library, node)
updateSelectedLibraries()
list.repaint()
}
})
return list
}
private fun createComponent(): DialogPanel {
startersComboBox.setMinimumAndPreferredWidth(200)
startersComboBox.renderer = SimpleListCellRenderer.create("", Starter::title)
startersComboBox.addItemListener { e ->
if (e.stateChange == ItemEvent.SELECTED) {
val newValue = e.item as? Starter
if (newValue != null) {
selectedStarterId = newValue.id
updateLibrariesList(newValue, false)
}
}
}
TreeSpeedSearch(librariesList, Convertor { treePath: TreePath ->
when (val dataObject = (treePath.lastPathComponent as DefaultMutableTreeNode).userObject) {
is LibraryCategory -> dataObject.title
is Library -> dataObject.title
else -> ""
}
})
librariesList.selectionModel.addTreeSelectionListener(TreeSelectionListener { e ->
val path = e.path
if (path != null && e.isAddedPath) {
when (val item = (path.lastPathComponent as? DefaultMutableTreeNode)?.userObject) {
is LibraryCategory -> libraryDescriptionPanel.update(item.title, item.description)
is Library -> libraryDescriptionPanel.update(item, null)
}
}
else {
libraryDescriptionPanel.reset()
}
})
val messages = starterSettings.customizedMessages
return panel {
if (starterContext.starterPack.starters.size > 1) {
row {
cell(isFullWidth = true) {
label(messages?.frameworkVersionLabel ?: JavaStartersBundle.message("title.project.version.label"))
component(startersComboBox)
}
}.largeGapAfter()
}
row {
label(messages?.dependenciesLabel ?: JavaStartersBundle.message("title.project.dependencies.label"))
}
row {
component(JPanel(GridBagLayout()).apply {
add(ScrollPaneFactory.createScrollPane(librariesList).apply {
preferredSize = Dimension(0, 0)
}, gridConstraint(0, 0))
add(JPanel(GridBagLayout()).apply {
border = JBUI.Borders.emptyLeft(UIUtil.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.empty(0, 0, UIUtil.DEFAULT_VGAP * 2, 0)
})
addToCenter(selectedLibrariesPanel)
}, gridConstraint(0, 1))
}, gridConstraint(1, 0))
}).constraints(push, grow)
}
}.withVisualPadding()
}
private fun createSelectedLibrariesPanel(): SelectedLibrariesPanel {
val panel = SelectedLibrariesPanel()
val messages = starterSettings.customizedMessages
panel.emptyText.text = messages?.noDependenciesSelectedLabel ?: JavaStartersBundle.message("hint.dependencies.not.selected")
panel.libraryRemoveListener = { libraryInfo ->
// do not remove from selectedLibraryIds directly, library can be included
walkCheckedTree(getLibrariesRoot()) {
if (it.userObject == libraryInfo && it.isEnabled) {
librariesList.setNodeState(it, false)
}
}
updateSelectedLibraries()
}
return panel
}
private fun updateSelectedLibraries() {
val selected = mutableListOf<LibraryInfo>()
walkCheckedTree(getLibrariesRoot()) {
val library = (it.userObject as? Library)
if (library != null && it.isChecked) {
selected.add(library)
}
}
selectedLibrariesPanel.update(selected)
}
private fun getLibrariesRoot(): CheckedTreeNode? {
return librariesList.model.root as? CheckedTreeNode
}
override fun _init() {
super._init()
if (topLevelPanel.componentCount == 0) {
// create UI only on first show
topLevelPanel.addToCenter(contentPanel)
}
// step became visible
val starterPack = starterContext.starterPack
startersComboBox.model = DefaultComboBoxModel(starterPack.starters.toTypedArray())
val initial = selectedStarterId == null
val selectedStarter = when (val previouslySelectedStarter = starterContext.starter?.id) {
null -> starterPack.starters.firstOrNull()
else -> starterPack.starters.find { it.id == previouslySelectedStarter }
}
if (selectedStarter != null) {
startersComboBox.selectedItem = selectedStarter
selectedStarterId = selectedStarter.id
selectedLibraryIds.clear()
for (libraryId in starterContext.libraryIds) {
val lib = selectedStarter.libraries.find { it.id == libraryId }
if (lib != null) {
selectedLibraryIds.add(libraryId)
selectedLibraryIds.removeAll(lib.includesLibraries)
}
}
updateLibrariesList(selectedStarter, initial)
}
}
private fun updateLibrariesList(starter: Starter, init: Boolean) {
val librariesRoot = CheckedTreeNode()
val categoryNodes = mutableMapOf<LibraryCategory, DefaultMutableTreeNode>()
val selectedLibraries = mutableListOf<Pair<Library, CheckedTreeNode>>()
val libraryNodes = mutableListOf<CheckedTreeNode>()
for (library in starter.libraries) {
if (!moduleBuilder.isDependencyAvailableInternal(starter, library)) continue
val libraryNode = CheckedTreeNode(library)
if (library.isRequired || library.isDefault && init) {
selectedLibraryIds.add(library.id)
}
libraryNode.isChecked = selectedLibraryIds.contains(library.id)
libraryNode.isEnabled = !library.isRequired
if (library.category == null) {
librariesRoot.add(libraryNode)
}
else {
val categoryNode = categoryNodes.getOrPut(library.category) {
val newCategoryNode = DefaultMutableTreeNode(library.category, true)
librariesRoot.add(newCategoryNode)
newCategoryNode
}
categoryNode.add(libraryNode)
}
libraryNodes.add(libraryNode)
if (libraryNode.isChecked) {
selectedLibraries.add(library to libraryNode)
}
}
val starterLibraryIds = starter.libraries.map { it.id }.toSet()
selectedLibraryIds.removeIf { !starterLibraryIds.contains(it) }
librariesList.model = DefaultTreeModel(librariesRoot)
for ((_, node) in categoryNodes) {
librariesList.expandPath(TreeUtil.getPath(librariesRoot, node))
}
if (libraryNodes.isNotEmpty()) {
librariesList.selectionModel.addSelectionPath(TreeUtil.getPath(librariesRoot, libraryNodes.first()))
}
for ((library, node) in selectedLibraries) {
updateIncludedLibraries(library, node)
}
updateSelectedLibraries()
}
private fun updateIncludedLibraries(library: Library, node: CheckedTreeNode) {
if (library.includesLibraries.isNotEmpty()) {
val rootNode = librariesList.model.root as? CheckedTreeNode
if (rootNode != null) {
for (child in rootNode.children()) {
if (child is CheckedTreeNode) {
updateNodeIncluded(child, library, node)
}
else if (child is DefaultMutableTreeNode) {
for (groupChild in child.children()) {
updateNodeIncluded(groupChild, library, node)
}
}
}
}
}
}
private fun updateNodeIncluded(child: Any?, library: Library, node: CheckedTreeNode) {
val checkedNode = child as? CheckedTreeNode
val nodeLibrary = checkedNode?.userObject as? Library ?: return
if (library.includesLibraries.contains(nodeLibrary.id)) {
checkedNode.isChecked = node.isChecked
checkedNode.isEnabled = !node.isChecked
}
}
}

View File

@@ -0,0 +1,31 @@
package com.intellij.ide.starters.remote
import com.intellij.ide.starters.shared.*
import com.intellij.openapi.util.UserDataHolderBase
class WebStarterContext : UserDataHolderBase() {
var name: String = DEFAULT_MODULE_NAME
var group: String = DEFAULT_MODULE_GROUP
var artifact: String = DEFAULT_MODULE_ARTIFACT
var version: String = DEFAULT_MODULE_VERSION
var isCreatingNewProject: Boolean = false
lateinit var serverUrl: String
lateinit var serverOptions: WebStarterServerOptions
lateinit var language: StarterLanguage
var frameworkVersion: WebStarterFrameworkVersion? = null
var projectType: StarterProjectType? = null
var packageName: String = DEFAULT_PACKAGE_NAME
var languageLevel: StarterLanguageLevel? = null
var packaging: StarterAppPackaging? = null
var applicationType: StarterAppType? = null
var testFramework: StarterTestRunner? = null
var includeExamples: Boolean = true
val dependencies: MutableSet<WebStarterDependency> = HashSet()
var result: DownloadResult? = null
}

View File

@@ -0,0 +1,374 @@
package com.intellij.ide.starters.remote
import com.google.gson.JsonElement
import com.google.gson.JsonNull
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import com.google.gson.stream.JsonReader
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.remote.wizard.WebStarterInitialStep
import com.intellij.ide.starters.remote.wizard.WebStarterLibrariesStep
import com.intellij.ide.starters.shared.*
import com.intellij.ide.util.projectWizard.*
import com.intellij.notification.Notification
import com.intellij.notification.NotificationAction
import com.intellij.notification.NotificationType
import com.intellij.openapi.Disposable
import com.intellij.openapi.actionSystem.AnActionEvent
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.logger
import com.intellij.openapi.extensions.PluginId
import com.intellij.openapi.externalSystem.model.ExternalSystemDataKeys
import com.intellij.openapi.module.JavaModuleType
import com.intellij.openapi.module.Module
import com.intellij.openapi.module.ModuleType
import com.intellij.openapi.module.StdModuleTypes
import com.intellij.openapi.options.ConfigurationException
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.*
import com.intellij.openapi.roots.LanguageLevelModuleExtension
import com.intellij.openapi.roots.ModifiableRootModel
import com.intellij.openapi.roots.ProjectRootManager
import com.intellij.openapi.roots.ui.configuration.ModulesProvider
import com.intellij.openapi.startup.StartupManager
import com.intellij.openapi.ui.Messages
import com.intellij.openapi.updateSettings.impl.pluginsAdvertisement.installAndEnable
import com.intellij.openapi.updateSettings.impl.pluginsAdvertisement.notificationGroup
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.util.text.StringUtil
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VfsUtil
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.util.concurrent.TimeUnit
import javax.swing.Icon
abstract class WebStarterModuleBuilder : ModuleBuilder() {
protected val starterContext: WebStarterContext = WebStarterContext()
private val starterSettings: StarterWizardSettings by lazy { createSettings() }
override fun getModuleType(): ModuleType<*> = StdModuleTypes.JAVA
override fun getParentGroup(): String = JavaModuleType.BUILD_TOOLS_GROUP
override fun getWeight(): Int = JavaModuleBuilder.BUILD_SYSTEM_WEIGHT + 10
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()
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()
)
}
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)
}
internal fun getUserAgentInternal(): String? = getUserAgent()
protected open fun getUserAgent(): String? {
return ApplicationNamesInfo.getInstance().fullProductName + "/" + ApplicationInfo.getInstance().fullVersion
}
protected open fun getCustomizedMessages(): CustomizedMessages? = null
@RequiresBackgroundThread
internal 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
}
internal fun getGeneratorUrlInternal(serverUrl: String, starterContext: WebStarterContext): Url {
return composeGeneratorUrl(serverUrl, starterContext)
}
protected abstract fun composeGeneratorUrl(serverUrl: String, starterContext: WebStarterContext): Url
@RequiresBackgroundThread
protected abstract fun extractGeneratorResult(tempZipFile: File, contentEntryDir: File)
protected open fun getPluginRecommendations(): List<PluginRecommendation> = emptyList()
protected open fun isReformatAfterCreation(project: Project): Boolean = true
@Throws(ConfigurationException::class)
override fun setupModule(module: Module) {
super.setupModule(module)
val project = module.project
if (starterContext.isCreatingNewProject) {
// Needed to ignore postponed project refresh
project.putUserData(ExternalSystemDataKeys.NEWLY_CREATED_PROJECT, java.lang.Boolean.TRUE)
project.putUserData(ExternalSystemDataKeys.NEWLY_IMPORTED_PROJECT, java.lang.Boolean.TRUE)
}
StartupManager.getInstance(project).runAfterOpened {
// a hack to avoid "Assertion failed: Network shouldn't be accessed in EDT or inside read action"
ApplicationManager.getApplication().invokeLater({ extractAndImport(module) },
ModalityState.NON_MODAL, module.disposed)
}
}
override fun setupRootModel(modifiableRootModel: ModifiableRootModel) {
val project = modifiableRootModel.project
val sdk = if (moduleJdk != null) moduleJdk else getProjectJdk(project)
if (sdk != null) {
if (!starterContext.isCreatingNewProject && sdk == getProjectJdk(project)) {
modifiableRootModel.inheritSdk()
}
else {
modifiableRootModel.sdk = sdk
}
}
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 getProjectJdk(project: Project): Sdk? {
return ProjectRootManager.getInstance(project).projectSdk
}
private fun extractAndImport(module: Module) {
ProgressManager.getInstance().runProcessWithProgressSynchronously(
{
try {
extractTemplate()
}
catch (e: Exception) {
logger<WebStarterModuleBuilder>().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, presentableName)
},
3, TimeUnit.SECONDS)
}
}, JavaStartersBundle.message("message.state.preparing.template"), true, module.project)
LocalFileSystem.getInstance().refresh(false) // to avoid IDEA-232806
preprocessModule(module)
if (isReformatAfterCreation(module.project)) {
ReformatCodeProcessor(module.project, module, false).run()
}
openSampleFiles(module, getFilePathsToOpen())
importModule(module)
verifyIdePlugins(module.project)
}
private fun preprocessModule(module: Module) {
StarterModuleBuilder.preprocessModule(module, this, starterContext.frameworkVersion?.id)
}
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))
}
val vf = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(contentEntryDir)
VfsUtil.markDirtyAndRefresh(false, true, false, vf)
}
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
notificationGroup.createNotification(
IdeBundle.message("plugins.advertiser.plugins.suggestions.title"),
IdeBundle.message("plugins.advertiser.plugins.suggestions.text"),
NotificationType.INFORMATION,
null,
).addAction(object : NotificationAction(IdeBundle.message("plugins.advertiser.action.enable.plugins")) {
override fun actionPerformed(e: AnActionEvent, notification: Notification) {
installAndEnable(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): JsonElement {
return HttpRequests.request(url)
.userAgent(getUserAgent())
.accept(accept)
.connectTimeout(10000)
.connect(RequestProcessor { request ->
val reader = try {
request.reader
}
catch (e: IOException) {
logger<WebStarterModuleBuilder>().info("IOException loading JSON response from " + request.url, e)
throw IOException(HttpRequests.createErrorMessage(e, request, false), e)
}
val jsonRootElement = try {
JsonParser.parseReader(JsonReader(reader).apply {
isLenient = true
})
}
catch (e: Throwable) {
logger<WebStarterModuleBuilder>().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")
})
}
fun JsonObject.getNullable(field: String): JsonElement? {
val element = this.get(field)
if (element is JsonNull) {
return null
}
return element
}
}

View File

@@ -0,0 +1,137 @@
@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 org.jetbrains.annotations.Nls
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
)
sealed class DependencyState
class DependencyUnavailable(
@Nls(capitalization = Nls.Capitalization.Sentence)
val message: String?,
@NlsSafe
val hint: String? = null
) : DependencyState()
object DependencyAvailable : DependencyState()
fun addStarterNetworkDelay() {
// todo remove
}
@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)

View File

@@ -0,0 +1,4 @@
@ApiStatus.Experimental
package com.intellij.ide.starters.remote;
import org.jetbrains.annotations.ApiStatus;

View File

@@ -0,0 +1,582 @@
package com.intellij.ide.starters.remote.wizard
import com.intellij.ide.starters.JavaStartersBundle
import com.intellij.ide.starters.remote.*
import com.intellij.ide.starters.shared.*
import com.intellij.ide.starters.shared.ValidationFunctions.*
import com.intellij.icons.AllIcons
import com.intellij.ide.BrowserUtil
import com.intellij.ide.util.PropertiesComponent
import com.intellij.ide.util.projectWizard.ModuleWizardStep
import com.intellij.ide.util.projectWizard.WizardContext
import com.intellij.openapi.Disposable
import com.intellij.openapi.actionSystem.DefaultActionGroup
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.properties.GraphPropertyImpl.Companion.graphProperty
import com.intellij.openapi.observable.properties.PropertyGraph
import com.intellij.openapi.observable.properties.map
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.roots.ui.configuration.sdkComboBox
import com.intellij.openapi.roots.ui.configuration.projectRoot.ProjectSdksModel
import com.intellij.openapi.roots.ui.configuration.validateJavaVersion
import com.intellij.openapi.roots.ui.configuration.validateSdk
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.io.FileUtil
import com.intellij.ui.InplaceButton
import com.intellij.ui.SimpleListCellRenderer
import com.intellij.ui.components.ActionLink
import com.intellij.ui.layout.*
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.File
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.JTextField
import javax.swing.SwingUtilities
open class WebStarterInitialStep(contextProvider: WebStarterContextProvider) : ModuleWizardStep() {
protected val moduleBuilder: WebStarterModuleBuilder = contextProvider.moduleBuilder
protected val wizardContext: WizardContext = contextProvider.wizardContext
protected val starterContext: WebStarterContext = contextProvider.starterContext
protected val starterSettings: StarterWizardSettings = contextProvider.settings
protected val parentDisposable: Disposable = contextProvider.parentDisposable
private val validatedTextComponents: MutableList<JTextField> = mutableListOf()
protected val propertyGraph: PropertyGraph = PropertyGraph()
private val entityNameProperty: GraphProperty<String> = propertyGraph.graphProperty(::suggestName)
private val locationProperty: GraphProperty<String> = propertyGraph.graphProperty(::suggestLocationByName)
private val groupIdProperty: GraphProperty<String> = propertyGraph.graphProperty { starterContext.group }
private val artifactIdProperty: GraphProperty<String> = propertyGraph.graphProperty { entityName }
private val packageNameProperty: GraphProperty<String> = propertyGraph.graphProperty { starterContext.packageName }
private val sdkProperty: GraphProperty<Sdk?> = propertyGraph.graphProperty { null }
private val projectTypeProperty: GraphProperty<StarterProjectType?> = propertyGraph.graphProperty { starterContext.projectType }
private val languageProperty: GraphProperty<StarterLanguage> = propertyGraph.graphProperty { starterContext.language }
private val packagingProperty: GraphProperty<StarterAppPackaging?> = propertyGraph.graphProperty { starterContext.packaging }
private val testFrameworkProperty: GraphProperty<StarterTestRunner?> = propertyGraph.graphProperty { starterContext.testFramework }
private val languageLevelProperty: GraphProperty<StarterLanguageLevel?> = propertyGraph.graphProperty { starterContext.languageLevel }
private val applicationTypeProperty: GraphProperty<StarterAppType?> = propertyGraph.graphProperty { starterContext.applicationType }
private val exampleCodeProperty: GraphProperty<Boolean> = propertyGraph.graphProperty { starterContext.includeExamples }
private var entityName: String by entityNameProperty.map { it.trim() }
private var location: String by locationProperty
private var groupId: String by groupIdProperty.map { it.trim() }
private var artifactId: String by artifactIdProperty.map { it.trim() }
private var languageLevel: StarterLanguageLevel? by languageLevelProperty
private var packageName: String by packageNameProperty.map { it.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 sdkModel: ProjectSdksModel = ProjectSdksModel()
private val languageLevelsModel: DefaultComboBoxModel<StarterLanguageLevel> = DefaultComboBoxModel<StarterLanguageLevel>()
private val applicationTypesModel: DefaultComboBoxModel<StarterAppType> = DefaultComboBoxModel<StarterAppType>()
private lateinit var projectTypesSelector: ButtonSelectorToolbar
private lateinit var packagingTypesSelector: ButtonSelectorToolbar
private lateinit var languagesSelector: ButtonSelectorToolbar
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
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 {
sdkModel.disposeUIResources()
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()
wizardContext.projectName = entityName
wizardContext.setProjectFileDirectory(location)
val sdk = sdkProperty.get()
if (wizardContext.project == null) {
wizardContext.projectJdk = sdk
}
else {
moduleBuilder.moduleJdk = sdk
}
}
private fun suggestName(): String {
return suggestName(starterContext.artifact)
}
private fun suggestName(prefix: String): String {
val projectFileDirectory = File(wizardContext.projectFileDirectory)
return FileUtil.createSequentFileName(projectFileDirectory, prefix, "")
}
private fun suggestLocationByName(): String {
return FileUtil.join(wizardContext.projectFileDirectory, entityName)
}
private fun suggestPackageName(): String {
return "${groupId.toLowerCase()}.${sanitizePackage(artifactId)}"
}
private fun createComponent(): DialogPanel {
entityNameProperty.dependsOn(locationProperty) { File(location).name }
entityNameProperty.dependsOn(artifactIdProperty) { artifactId }
locationProperty.dependsOn(entityNameProperty, ::suggestLocationByName)
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 {
cell(isFullWidth = true) {
label(JavaStartersBundle.message("title.project.server.url.label"))
component(serverUrlLink)
component(serverSettingsButton)
component(retryButton)
component(progressIcon)
}
}.largeGapAfter()
row(JavaStartersBundle.message("title.project.name.label")) {
textField(entityNameProperty)
.growPolicy(GrowPolicy.SHORT_TEXT)
.withSpecialValidation(CHECK_NOT_EMPTY, CHECK_SIMPLE_NAME_FORMAT)
.focused()
}.largeGapAfter()
row(JavaStartersBundle.message("title.project.location.label")) {
projectLocationField(locationProperty, wizardContext)
.withSpecialValidation(listOf(CHECK_NOT_EMPTY, CHECK_LOCATION_FOR_ERROR), CHECK_LOCATION_FOR_WARNING)
}.largeGapAfter()
addFieldsBefore(this)
if (starterSettings.languages.size > 1) {
row(JavaStartersBundle.message("title.project.language.label")) {
languagesSelector = buttonSelector(starterSettings.languages, languageProperty) { it.title }
}.largeGapAfter()
}
if (starterSettings.projectTypes.isNotEmpty()) {
val messages = starterSettings.customizedMessages
row(messages?.projectTypeLabel ?: JavaStartersBundle.message("title.project.type.label")) {
projectTypesSelector = buttonSelector(starterSettings.projectTypes, projectTypeProperty) { it?.title ?: "" }
}.largeGapAfter()
}
if (starterSettings.testFrameworks.isNotEmpty()) {
row(JavaStartersBundle.message("title.project.test.framework.label")) {
buttonSelector(starterSettings.testFrameworks, testFrameworkProperty) { it?.title ?: "" }
}.largeGapAfter()
}
row(JavaStartersBundle.message("title.project.group.label")) {
textField(groupIdProperty)
.growPolicy(GrowPolicy.SHORT_TEXT)
.withSpecialValidation(CHECK_NOT_EMPTY, CHECK_NO_WHITESPACES, CHECK_GROUP_FORMAT, CHECK_NO_RESERVED_WORDS)
}.largeGapAfter()
row(JavaStartersBundle.message("title.project.artifact.label")) {
textField(artifactIdProperty)
.growPolicy(GrowPolicy.SHORT_TEXT)
.withSpecialValidation(CHECK_NOT_EMPTY, CHECK_NO_WHITESPACES, CHECK_ARTIFACT_SIMPLE_FORMAT, CHECK_NO_RESERVED_WORDS)
}.largeGapAfter()
if (starterSettings.isPackageNameEditable) {
row(JavaStartersBundle.message("title.project.package.label")) {
textField(packageNameProperty)
.growPolicy(GrowPolicy.SHORT_TEXT)
.withSpecialValidation(CHECK_NOT_EMPTY, CHECK_NO_WHITESPACES, CHECK_NO_RESERVED_WORDS, CHECK_PACKAGE_NAME)
}.largeGapAfter()
}
if (starterSettings.applicationTypes.isNotEmpty()) {
row(JavaStartersBundle.message("title.project.app.type.label")) {
applicationTypesModel.addAll(starterSettings.applicationTypes)
comboBox(applicationTypesModel, applicationTypeProperty, SimpleListCellRenderer.create("") { it?.title ?: "" })
.growPolicy(GrowPolicy.SHORT_TEXT)
}.largeGapAfter()
}
row(JavaStartersBundle.message("title.project.sdk.label")) {
sdkComboBox(sdkModel, sdkProperty, wizardContext.project, moduleBuilder)
.growPolicy(GrowPolicy.SHORT_TEXT)
}.largeGapAfter()
if (starterSettings.languageLevels.isNotEmpty()) {
row(JavaStartersBundle.message("title.project.java.version.label")) {
languageLevelsModel.addAll(starterSettings.languageLevels)
comboBox(languageLevelsModel, languageLevelProperty, SimpleListCellRenderer.create("") { it?.title ?: "" })
}.largeGapAfter()
}
if (starterSettings.packagingTypes.isNotEmpty()) {
row(JavaStartersBundle.message("title.project.packaging.label")) {
packagingTypesSelector = buttonSelector(starterSettings.packagingTypes, packagingProperty) { it?.title ?: "" }
}.largeGapAfter()
}
if (starterSettings.isExampleCodeProvided) {
row {
checkBox(JavaStartersBundle.message("title.project.examples.label"), exampleCodeProperty)
}
}
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
}
if (!validateSdk(sdkProperty, sdkModel)) {
return false
}
if (!validateJavaVersion(sdkProperty, languageLevel?.javaVersion)) {
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()
}
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 { updatePropertiesWithServerOptions(it) }
return@Runnable
}
}
}, JavaStartersBundle.message("message.state.connecting.and.retrieving.options"), true, wizardContext.project)
return serverOptions != null
}
protected open fun addFieldsBefore(layout: LayoutBuilder) {}
protected open fun addFieldsAfter(layout: LayoutBuilder) {}
override fun _init() {
super._init()
if (serverOptions == null && currentRequest == null) {
@Suppress("HardCodedStringLiteral")
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 {
addStarterNetworkDelay()
val readyServerOptions = try {
moduleBuilder.getServerOptions(starterContext.serverUrl)
}
catch (e: Exception) {
if (e is IOException) {
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.wizard.contentComponent)
}
private fun getDisposed(): Condition<Any> = Condition<Any> { Disposer.isDisposed(parentDisposable) }
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()
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 (packageName == DEFAULT_PACKAGE_NAME && packageName != 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()
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())
val actionGroup = projectTypesSelector.actionGroup as DefaultActionGroup
actionGroup.removeAll()
actionGroup.addAll(types.map { ButtonSelectorAction(it, projectTypeProperty, it.title, it.description) })
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())
val actionGroup = packagingTypesSelector.actionGroup as DefaultActionGroup
actionGroup.removeAll()
actionGroup.addAll(types.map { ButtonSelectorAction(it, packagingProperty, it.title, it.description) })
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())
val actionGroup = languagesSelector.actionGroup as DefaultActionGroup
actionGroup.removeAll()
actionGroup.addAll(languages.map { ButtonSelectorAction(it, languageProperty, it.title, it.description) })
this.languages = languages
}
}
contentPanel.revalidate()
}
private fun sanitizePackage(input: String): String {
val fileName = FileUtil.sanitizeFileName(input, false)
return fileName
.replace(' ', '-')
.replace("-", "")
.toLowerCase()
}
@Suppress("SameParameterValue")
private fun <T : JComponent> CellBuilder<T>.withSpecialValidation(vararg errorValidations: TextValidationFunction): CellBuilder<T> {
return this.withSpecialValidation(errorValidations.asList(), null)
}
private fun <T : JComponent> CellBuilder<T>.withSpecialValidation(errorValidations: List<TextValidationFunction>,
warningValidation: TextValidationFunction?): CellBuilder<T> {
return withValidation(this, errorValidations, warningValidation, validatedTextComponents, parentDisposable)
}
}

View File

@@ -0,0 +1,550 @@
package com.intellij.ide.starters.remote.wizard
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.IdeBundle
import com.intellij.ide.util.projectWizard.ModuleWizardStep
import com.intellij.ide.util.projectWizard.WizardContext
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.GraphPropertyImpl.Companion.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.NlsSafe
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.util.text.StringUtil
import com.intellij.ui.*
import com.intellij.ui.components.JBLabel
import com.intellij.ui.layout.*
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 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.io.IOException
import java.net.URLConnection
import java.net.UnknownHostException
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
protected val wizardContext: WizardContext = contextProvider.wizardContext
protected val starterContext: WebStarterContext = contextProvider.starterContext
protected 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()
private val propertyGraph: PropertyGraph = PropertyGraph()
private var frameworkVersionProperty: GraphProperty<WebStarterFrameworkVersion?> = propertyGraph.graphProperty { 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 {
downloadResult(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)
}
@RequiresBackgroundThread
private fun downloadResult(progressIndicator: ProgressIndicator): DownloadResult {
addStarterNetworkDelay()
val tempFile = FileUtil.createTempFile(moduleBuilder.builderId, ".tmp", true)
val log = logger<WebStarterLibrariesStep>()
val url = moduleBuilder.getGeneratorUrlInternal(starterContext.serverUrl, starterContext).toExternalForm()
log.info("Loading project from ${url}")
return HttpRequests
.request(url)
.userAgent(moduleBuilder.getUserAgentInternal())
.connectTimeout(10000)
.isReadResponseOnError(true)
.connect(RequestProcessor { request ->
val connection: URLConnection = try {
request.connection
}
catch (e: IOException) {
log.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) {
log.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)
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
}
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())
}
}
private fun createComponent(): DialogPanel {
val messages = starterSettings.customizedMessages
selectedLibrariesPanel.emptyText.text = messages?.noDependenciesSelectedLabel
?: JavaStartersBundle.message("hint.dependencies.not.selected")
return panel(LCFlags.fillX, LCFlags.fillY) {
val frameworkVersions = getAvailableFrameworkVersions()
if (frameworkVersions.isNotEmpty()) {
row {
cell(isFullWidth = true) {
label(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, frameworkVersionProperty, SimpleListCellRenderer.create("") { it?.title ?: "" })
}
}
}.largeGapAfter()
}
row {
label(messages?.dependenciesLabel ?: JavaStartersBundle.message("title.project.dependencies.label"))
}
row {
component(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.empty(0, 0, DEFAULT_VGAP * 2, 0)
})
addToCenter(selectedLibrariesPanel)
}, gridConstraint(0, 1))
}, gridConstraint(1, 0))
}).constraints(push, grow)
}
}.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 {
GuiUtils.invokeLaterIfNeeded(Runnable {
currentSearchString = textField.text
loadLibrariesList()
librariesList.repaint()
}, getModalityState())
}))
}
})
return textField
}
protected fun getModalityState(): ModalityState {
return ModalityState.stateForComponent(wizardContext.wizard.contentComponent)
}
protected fun getDisposed(): Condition<Any> = Condition<Any> { Disposer.isDisposed(parentDisposable) }
private 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)
if (dependency.isDefault) {
libraryNode.isEnabled = false
}
else {
val state = getDependencyState(dependency)
libraryNode.isEnabled = 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 = state is DependencyAvailable
}
}
librariesList.repaint()
val selectedDependency = (librariesList.selectionPath?.lastPathComponent as? CheckedTreeNode)?.userObject
if (selectedDependency is WebStarterDependency) {
updateSelectedLibraryInfo(selectedDependency)
}
}
}

View File

@@ -0,0 +1,218 @@
package com.intellij.ide.starters.shared
import com.intellij.ide.starters.JavaStartersBundle
import com.intellij.ide.IdeBundle
import com.intellij.openapi.Disposable
import com.intellij.openapi.ui.*
import com.intellij.openapi.util.NlsSafe
import com.intellij.ui.CheckboxTreeBase
import com.intellij.ui.CheckedTreeNode
import com.intellij.ui.DocumentAdapter
import com.intellij.ui.IdeBorderFactory
import com.intellij.ui.layout.*
import com.intellij.util.ui.JBInsets
import java.awt.GridBagConstraints
import java.awt.event.ActionEvent
import java.awt.event.FocusEvent
import java.awt.event.FocusListener
import java.util.function.Supplier
import javax.swing.AbstractAction
import javax.swing.JComponent
import javax.swing.JTextField
import javax.swing.KeyStroke
import javax.swing.event.DocumentEvent
import javax.swing.tree.DefaultMutableTreeNode
import javax.swing.tree.TreeNode
const val NAME_FIELD_TOP_PADDING: Int = 15
fun DialogPanel.withVisualPadding(top: Int = 5): DialogPanel {
border = IdeBorderFactory.createEmptyBorder(JBInsets(top, 5, 0, 5))
return this
}
internal fun gridConstraint(col: Int, row: Int): GridBagConstraints {
return GridBagConstraints().apply {
fill = GridBagConstraints.BOTH
gridx = col
gridy = row
weightx = 1.0
weighty = 1.0
}
}
fun <T : JComponent> withValidation(
builder: CellBuilder<T>,
errorChecks: List<TextValidationFunction>,
warningChecks: TextValidationFunction?,
validatedTextComponents: MutableList<JTextField>,
parentDisposable: Disposable
): CellBuilder<T> {
if (errorChecks.isEmpty()) return builder
val textField = getJTextField(builder.component)
val validationFunc = Supplier<ValidationInfo?> {
val text = textField.text
for (validationUnit in errorChecks) {
val errorMessage = validationUnit.checkText(text)
if (errorMessage != null) {
return@Supplier ValidationInfo(errorMessage, textField)
}
}
if (warningChecks != null) {
val warningMessage = warningChecks.checkText(text)
if (warningMessage != null) {
return@Supplier ValidationInfo(warningMessage, textField).asWarning().withOKEnabled()
}
}
null
}
ComponentValidator(parentDisposable)
.withValidator(validationFunc)
.installOn(textField)
textField.document.addDocumentListener(object : DocumentAdapter() {
override fun textChanged(e: DocumentEvent) {
ComponentValidator.getInstance(textField).ifPresent { v: ComponentValidator -> v.updateInfo(null) }
}
})
// use FocusListener instead of Validator.andStartOnFocusLost(), because the second one can disable validation, that can cause
// not validating unfocused fields, that are depends on some other fields
textField.addFocusListener(object : FocusListener {
override fun focusGained(e: FocusEvent) {
// ignore
}
override fun focusLost(e: FocusEvent) {
revalidateAllAndHighlight(validatedTextComponents)
}
})
validatedTextComponents.add(textField)
return builder
}
fun validateFormFields(formParent: JComponent,
contentPanel: DialogPanel,
validatedComponents: List<JComponent>): Boolean {
// look for errors
var firstInvalidComponent: JComponent? = null
for (component in validatedComponents) {
ComponentValidator.getInstance(component).ifPresent { validator: ComponentValidator ->
validator.revalidate()
val validationInfo = validator.validationInfo
if (validationInfo != null && !validationInfo.warning) {
if (firstInvalidComponent == null) {
firstInvalidComponent = component
}
}
}
}
if (firstInvalidComponent != null) {
contentPanel.preferredFocusedComponent = firstInvalidComponent
return false
}
// look for warnings
val warnings = mutableListOf<ValidationInfo>()
for (component in validatedComponents) {
ComponentValidator.getInstance(component).ifPresent { validator: ComponentValidator ->
val validationInfo = validator.validationInfo
if (validationInfo != null && validationInfo.warning) {
warnings.add(validationInfo)
}
}
}
if (warnings.isNotEmpty()) {
val message = getWarningsMessage(warnings)
val answer = Messages.showOkCancelDialog(formParent, message,
IdeBundle.message("title.warning"),
Messages.getYesButton(),
Messages.getCancelButton(),
Messages.getWarningIcon())
if (answer != Messages.OK) {
return false
}
}
return true
}
private fun revalidateAllAndHighlight(validatedComponents: List<JComponent>) {
for (component in validatedComponents) {
ComponentValidator.getInstance(component).ifPresent { validator: ComponentValidator ->
validator.revalidate()
}
}
}
private fun getJTextField(component: JComponent): JTextField {
return when (component) {
is TextFieldWithBrowseButton -> component.textField
is JTextField -> component
else -> throw IllegalArgumentException()
}
}
@NlsSafe
private fun getWarningsMessage(warnings: MutableList<ValidationInfo>): String {
val message = StringBuilder()
if (warnings.size > 1) {
message.append(JavaStartersBundle.message("project.settings.warnings.group"))
for (warning in warnings) {
message.append("\n- ").append(warning.message)
}
}
else if (warnings.isNotEmpty()) {
message.append(warnings.first().message)
}
message.append("\n\n").append(JavaStartersBundle.message("project.settings.warnings.ignore"))
return message.toString()
}
internal fun walkCheckedTree(root: CheckedTreeNode?, visitor: (CheckedTreeNode) -> Unit) {
if (root == null) return
fun walkTreeNode(root: TreeNode, visitor: (CheckedTreeNode) -> Unit) {
if (root is CheckedTreeNode) {
visitor.invoke(root)
}
for (child in root.children()) {
walkTreeNode(child, visitor)
}
}
walkTreeNode(root, visitor)
}
internal fun enableEnterKeyHandling(list: CheckboxTreeBase) {
list.inputMap.put(KeyStroke.getKeyStroke("ENTER"), "pick-node")
list.actionMap.put("pick-node", object : AbstractAction() {
override fun actionPerformed(e: ActionEvent?) {
val selection = list.selectionPath
if (selection != null) {
if (selection.lastPathComponent is CheckedTreeNode) {
val node = selection.lastPathComponent as CheckedTreeNode
list.setNodeState(node, !node.isChecked)
}
else if (selection.lastPathComponent is DefaultMutableTreeNode) {
if (list.isExpanded(selection)) {
list.collapsePath(selection)
}
else {
list.expandPath(selection)
}
}
}
}
})
}

View File

@@ -0,0 +1,23 @@
package com.intellij.ide.starters.shared
import com.intellij.ide.starters.JavaStartersBundle
import com.intellij.icons.AllIcons
import org.jetbrains.annotations.Nls
import javax.swing.Icon
enum class LibraryLinkType(val icon: Icon?) {
GUIDE(AllIcons.Nodes.HomeFolder),
REFERENCE(AllIcons.Actions.Preview),
WEBSITE(AllIcons.Nodes.PpWeb),
SPECIFICATION(AllIcons.Nodes.PpWeb);
@Nls
fun getTitle(): String {
return when (this) {
GUIDE -> JavaStartersBundle.message("starter.link.guide")
REFERENCE -> JavaStartersBundle.message("starter.link.reference")
WEBSITE -> JavaStartersBundle.message("starter.link.website")
SPECIFICATION -> JavaStartersBundle.message("starter.link.specification")
}
}
}

View File

@@ -0,0 +1,109 @@
@file:JvmName("StarterSettings")
package com.intellij.ide.starters.shared
import com.intellij.openapi.projectRoots.JavaSdkVersion
import com.intellij.openapi.util.NlsContexts.DialogTitle
import com.intellij.openapi.util.NlsContexts.Label
import com.intellij.openapi.util.NlsSafe
data class StarterLanguage(
val id: String,
@NlsSafe val title: String,
val languageId: String,
val isBuiltIn: Boolean = false,
@NlsSafe val description: String? = null
)
data class StarterTestRunner(
val id: String,
@NlsSafe val title: String
)
data class StarterProjectType(
val id: String,
@NlsSafe val title: String,
@NlsSafe val description: String? = null
)
data class StarterAppType(
val id: String,
@NlsSafe val title: String
)
data class StarterAppPackaging(
val id: String,
@NlsSafe val title: String,
@NlsSafe val description: String? = null
)
data class StarterLanguageLevel(
val id: String,
@NlsSafe val title: String,
/**
* Version string that can be parsed with [JavaSdkVersion.fromVersionString].
*/
val javaVersion: String
)
class CustomizedMessages {
var projectTypeLabel: @Label String? = null
var serverUrlDialogTitle: @DialogTitle String? = null
var dependenciesLabel: @Label String? = null
var selectedDependenciesLabel: @Label String? = null
var noDependenciesSelectedLabel: @Label String? = null
var frameworkVersionLabel: @Label String? = null
}
class StarterWizardSettings(
val projectTypes: List<StarterProjectType>,
val languages: List<StarterLanguage>,
val isExampleCodeProvided: Boolean,
val isPackageNameEditable: Boolean,
val languageLevels: List<StarterLanguageLevel>,
val defaultLanguageLevel: StarterLanguageLevel?,
val packagingTypes: List<StarterAppPackaging>,
val applicationTypes: List<StarterAppType>,
val testFrameworks: List<StarterTestRunner>,
val customizedMessages: CustomizedMessages?
)
class PluginRecommendation(
val pluginId: String,
val dependencyIds: List<String>
) {
constructor(pluginId: String, vararg dependencyIds: String) : this(pluginId, dependencyIds.toList())
}
interface LibraryInfo {
@get:NlsSafe
val title: String
val description: String?
val links: List<LibraryLink>
val isRequired: Boolean
val isDefault: Boolean
}
class LibraryLink(
val type: LibraryLinkType,
@NlsSafe
val url: String,
@NlsSafe
val title: String? = null
)
const val DEFAULT_MODULE_NAME: String = "demo"
const val DEFAULT_MODULE_GROUP: String = "com.example"
const val DEFAULT_MODULE_ARTIFACT: String = "demo"
const val DEFAULT_MODULE_VERSION: String = "1.0-SNAPSHOT"
const val DEFAULT_PACKAGE_NAME: String = "$DEFAULT_MODULE_GROUP.$DEFAULT_MODULE_ARTIFACT"
val JAVA_STARTER_LANGUAGE: StarterLanguage = StarterLanguage("java", "Java", "JAVA", true)
val KOTLIN_STARTER_LANGUAGE: StarterLanguage = StarterLanguage("kotlin", "Kotlin", "kotlin")
val GROOVY_STARTER_LANGUAGE: StarterLanguage = StarterLanguage("groovy", "Groovy", "Groovy")
val MAVEN_PROJECT: StarterProjectType = StarterProjectType("maven", "Maven")
val GRADLE_PROJECT: StarterProjectType = StarterProjectType("gradle", "Gradle")
val JUNIT_TEST_RUNNER: StarterTestRunner = StarterTestRunner("junit", "JUnit")
val TESTNG_TEST_RUNNER: StarterTestRunner = StarterTestRunner("testng", "TestNG")

View File

@@ -0,0 +1,9 @@
package com.intellij.ide.starters.shared;
import org.jetbrains.annotations.Nls;
@FunctionalInterface
public interface TextValidationFunction {
@Nls(capitalization = Nls.Capitalization.Sentence)
String checkText(String fieldText);
}

View File

@@ -0,0 +1,169 @@
package com.intellij.ide.starters.shared;
import com.intellij.ide.starters.JavaStartersBundle;
import com.intellij.ide.impl.ProjectUtil;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.ProjectManager;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.psi.impl.PsiNameHelperImpl;
import java.io.File;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.regex.Pattern;
public final class ValidationFunctions {
public static final TextValidationFunction CHECK_PACKAGE_NAME = (fieldText) -> {
if (!PsiNameHelperImpl.getInstance().isQualifiedName(fieldText)) {
return JavaStartersBundle.message("message.some.string.is.not.a.valid.package.name", fieldText);
}
return null;
};
public static final TextValidationFunction CHECK_SIMPLE_NAME_FORMAT = new TextValidationFunction() {
private final Pattern myPattern = Pattern.compile("[a-zA-Z0-9-._ ]*"); // IDEA-235441
@Override
public String checkText(String fieldText) {
if (!myPattern.matcher(fieldText).matches()) {
return JavaStartersBundle.message(
"message.only.latin.characters.digits.spaces.and.some.other.symbols.are.allowed.here");
}
return null;
}
};
public static final TextValidationFunction CHECK_NOT_EMPTY = (fieldText) -> {
if (fieldText.isEmpty()) {
return JavaStartersBundle.message("message.field.must.be.set");
}
return null;
};
public static final TextValidationFunction CHECK_NO_WHITESPACES = (fieldText) -> {
if (fieldText.contains(" ")) {
return JavaStartersBundle.message("message.whitespaces.are.not.allowed.here");
}
return null;
};
// IDEA-235887 prohibit using some words reserved by Windows in group and artifact fields
public static final TextValidationFunction CHECK_NO_RESERVED_WORDS =
new TextValidationFunction() {
private final Pattern myPattern = Pattern.compile("(^|[ .])(con|prn|aux|nul|com\\d|lpt\\d)($|[ .])",
Pattern.CASE_INSENSITIVE);
@Override
public String checkText(String fieldText) {
if (myPattern.matcher(fieldText).find()) {
return JavaStartersBundle.message("message.some.parts.are.not.allowed.here");
}
return null;
}
};
// This validation describes the most common and important rules for all Web Starters implementations
public static final TextValidationFunction CHECK_GROUP_FORMAT = new TextValidationFunction() {
private final Pattern myPatternForEntireText = Pattern.compile("[a-zA-Z\\d_.]*");
private final Pattern myPatternForOneWord = Pattern.compile("[a-zA-Z_].*");
@Override
public String checkText(String fieldText) {
if (!myPatternForEntireText.matcher(fieldText).matches()) {
return JavaStartersBundle.message("message.only.latin.characters.digits.and.some.other.symbols.are.allowed.here");
}
char firstSymbol = fieldText.charAt(0);
char lastSymbol = fieldText.charAt(fieldText.length() - 1);
if (firstSymbol == '.' || lastSymbol == '.') {
return JavaStartersBundle.message("message.must.not.start.or.end.with.dot");
}
if (fieldText.contains("..")) {
return JavaStartersBundle.message("message.must.not.contain.double.dot.sequences");
}
String[] wordsBetweenDots = fieldText.split("\\.");
for (String word : wordsBetweenDots) {
if (!myPatternForOneWord.matcher(word).matches()) {
return JavaStartersBundle
.message("message.part.is.incorrect.and.must.start.with.latin.character.or.some.other.symbols", word);
}
}
return null;
}
};
public static final TextValidationFunction CHECK_ARTIFACT_SIMPLE_FORMAT = new TextValidationFunction() {
private final Pattern myUsedSymbolsCheckPattern = Pattern.compile("[a-zA-Z0-9-_]*");
private final Pattern myFirstSymbolCheckPattern = Pattern.compile("[a-zA-Z_].*");
@Override
public String checkText(String fieldText) {
if (!myUsedSymbolsCheckPattern.matcher(fieldText).matches()) {
return JavaStartersBundle.message("message.allowed.symbols.for.check.artifact.simple.format");
}
if (!myFirstSymbolCheckPattern.matcher(fieldText).matches()) {
return JavaStartersBundle.message("message.allows.first.symbol.for.check.artifact.simple.format");
}
return null;
}
};
public static final TextValidationFunction CHECK_LOCATION_FOR_WARNING = fieldText -> {
File file = Paths.get(FileUtil.expandUserHome(fieldText)).toFile();
if (file.exists()) {
String[] children = file.list();
if (children != null && children.length > 0) {
return JavaStartersBundle.message("message.directory.not.empty.warning");
}
}
return null;
};
public static final TextValidationFunction CHECK_LOCATION_FOR_ERROR = fieldText -> {
Path locationPath;
try {
locationPath = Paths.get(fieldText);
} catch (InvalidPathException e) {
return JavaStartersBundle.message("message.specified.path.is.illegal");
}
for (Project project : ProjectManager.getInstance().getOpenProjects()) {
if (ProjectUtil.isSameProject(locationPath, project)) {
return JavaStartersBundle.message("message.directory.already.taken.error", project.getName());
}
}
File file = locationPath.toFile();
if (file.exists()) {
if (!file.canWrite()) {
return JavaStartersBundle.message("message.directory.not.writable.error");
}
String[] children = file.list();
if (children == null) {
return JavaStartersBundle.message("message.file.not.directory.error");
}
}
return null;
};
// This validation describes the most common and important rules for all Web Starters implementations
public static final TextValidationFunction CHECK_ARTIFACT_FORMAT_FOR_WEB = new TextValidationFunction() {
private final Pattern myPattern = Pattern.compile("[a-z0-9-._]*");
@Override
public String checkText(String fieldText) {
if (!myPattern.matcher(fieldText).matches()) {
return JavaStartersBundle.message("message.only.lowercase.latin.characters.digits.and.some.other.symbols.are.allowed.here");
}
if (fieldText.charAt(0) < 'a' || fieldText.charAt(0) > 'z') {
return JavaStartersBundle.message("message.must.start.with.lowercase.latin.character");
}
return null;
}
};
}

View File

@@ -0,0 +1,4 @@
@ApiStatus.Experimental
package com.intellij.ide.starters.shared;
import org.jetbrains.annotations.ApiStatus;

View File

@@ -0,0 +1,72 @@
package com.intellij.ide.starters.shared.ui
import com.intellij.ide.starters.JavaStartersBundle
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.CommonShortcuts
import com.intellij.ui.JBColor
import com.intellij.ui.SearchTextField
import com.intellij.ui.scale.JBUIScale
import com.intellij.util.ui.JBUI
import java.awt.Dimension
import java.awt.event.KeyEvent
import javax.swing.JComponent
internal class LibrariesSearchTextField : SearchTextField() {
var list: JComponent? = null
init {
textEditor.putClientProperty("JTextField.Search.Gap", JBUIScale.scale(6))
textEditor.putClientProperty("JTextField.Search.GapEmptyText", JBUIScale.scale(-1))
textEditor.border = JBUI.Borders.empty()
textEditor.emptyText.text = JavaStartersBundle.message("hint.library.search")
border = JBUI.Borders.customLine(JBColor.border(), 1, 1, 0, 1)
}
override fun preprocessEventForTextField(event: KeyEvent): Boolean {
val keyCode: Int = event.keyCode
val id: Int = event.id
if ((keyCode == KeyEvent.VK_DOWN || keyCode == KeyEvent.VK_UP || keyCode == KeyEvent.VK_ENTER) && id == KeyEvent.KEY_PRESSED
&& handleListEvents(event)) {
return true
}
return super.preprocessEventForTextField(event)
}
private fun handleListEvents(event: KeyEvent): Boolean {
val selectionTracker = list
if (selectionTracker != null) {
selectionTracker.dispatchEvent(event)
return true
}
return false
}
override fun getPreferredSize(): Dimension {
val size = super.getPreferredSize()
size.height = JBUIScale.scale(30)
return size
}
override fun toClearTextOnEscape(): Boolean {
object : AnAction() {
override fun update(e: AnActionEvent) {
e.presentation.isEnabled = text.isNotEmpty()
}
override fun actionPerformed(e: AnActionEvent) {
text = ""
}
init {
isEnabledInModalContext = true
}
}.registerCustomShortcutSet(CommonShortcuts.ESCAPE, this)
return false
}
}

View File

@@ -0,0 +1,199 @@
package com.intellij.ide.starters.shared.ui
import com.intellij.ide.starters.JavaStartersBundle
import com.intellij.ide.starters.shared.LibraryInfo
import com.intellij.icons.AllIcons
import com.intellij.openapi.roots.ui.componentsList.components.ScrollablePanel
import com.intellij.openapi.ui.ex.MultiLineLabel
import com.intellij.openapi.util.NlsSafe
import com.intellij.ui.HyperlinkLabel
import com.intellij.ui.JBColor
import com.intellij.ui.components.JBLabel
import com.intellij.ui.components.panels.VerticalLayout
import com.intellij.util.ui.*
import com.intellij.util.ui.UIUtil.DEFAULT_HGAP
import com.intellij.util.ui.UIUtil.DEFAULT_VGAP
import com.intellij.util.ui.components.BorderLayoutPanel
import java.awt.*
import javax.swing.JPanel
import javax.swing.JTextArea
import javax.swing.SwingUtilities
import kotlin.math.max
internal class LibraryDescriptionPanel : ScrollablePanel(VerticalLayout(DEFAULT_VGAP)) {
private val descriptionHeader: JBLabel = JBLabel()
private val descriptionText: JTextArea = JTextArea()
private val descriptionVersion: MultiLineLabel = MultiLineLabel()
private val linksPanel: JPanel = JPanel(WrappedFlowLayout())
private val emptyState: StatusText = object : StatusText(this) {
override fun isStatusVisible(): Boolean {
return UIUtil.uiChildren(this@LibraryDescriptionPanel)
.filter { obj: Component -> obj.isVisible }
.isEmpty
}
}
init {
this.border = JBUI.Borders.empty(DEFAULT_VGAP)
val headerPanel = JPanel(BorderLayout())
descriptionHeader.font = StartupUiUtil.getLabelFont().deriveFont(Font.BOLD)
descriptionHeader.border = JBUI.Borders.empty(DEFAULT_VGAP, 0)
descriptionVersion.icon = AllIcons.General.BalloonWarning
descriptionVersion.border = JBUI.Borders.empty(DEFAULT_VGAP, 0, DEFAULT_VGAP * 2, 0)
descriptionVersion.isVisible = false
headerPanel.add(descriptionHeader, BorderLayout.NORTH)
headerPanel.add(descriptionVersion, BorderLayout.CENTER)
add(headerPanel)
descriptionText.background = JBColor.PanelBackground
descriptionText.isFocusable = false
descriptionText.lineWrap = true
descriptionText.wrapStyleWord = true
descriptionText.isEditable = false
descriptionText.font = JBUI.Fonts.label()
add(descriptionText)
linksPanel.border = JBUI.Borders.emptyTop(DEFAULT_VGAP * 2)
add(linksPanel)
emptyState.text = JavaStartersBundle.message("hint.no.library.selected")
showEmptyState()
}
fun update(library: LibraryInfo, @NlsSafe versionConstraint: String?) {
descriptionHeader.text = library.title
descriptionVersion.text = versionConstraint ?: ""
descriptionVersion.isVisible = versionConstraint != null
descriptionText.text = library.description
addDescriptionLinks(linksPanel, library)
showDescriptionUi()
}
fun update(@NlsSafe title: String, description: String?) {
descriptionHeader.text = title
descriptionVersion.text = ""
descriptionVersion.isVisible = false
descriptionText.text = description
linksPanel.removeAll()
showDescriptionUi()
}
fun reset() {
descriptionHeader.text = ""
descriptionVersion.text = ""
descriptionText.text = ""
descriptionVersion.isVisible = false
linksPanel.removeAll()
showEmptyState()
}
private fun showEmptyState() {
for (component in this.components) {
component.isVisible = false
}
revalidate()
repaint()
}
private fun showDescriptionUi() {
for (component in this.components) {
component.isVisible = true
}
revalidate()
repaint()
}
override fun paintComponent(g: Graphics?) {
super.paintComponent(g)
emptyState.paint(this, g)
}
override fun getComponentGraphics(graphics: Graphics?): Graphics {
return JBSwingUtilities.runGlobalCGTransform(this, super.getComponentGraphics(graphics))
}
private fun addDescriptionLinks(linksPanel: JPanel, item: LibraryInfo) {
linksPanel.removeAll()
for (link in item.links) {
if (link.url.contains('{')) continue // URL templates are not supported
val linkLabel = HyperlinkLabel(link.title ?: link.type.getTitle())
linkLabel.font = JBUI.Fonts.smallFont()
linkLabel.setHyperlinkTarget(link.url)
linkLabel.toolTipText = link.url
linksPanel.add(BorderLayoutPanel().apply {
addToCenter(linkLabel)
border = JBUI.Borders.empty(0, 0, 0, DEFAULT_HGAP / 2)
})
}
linksPanel.revalidate()
linksPanel.repaint()
}
// do not add horizontal gap - it is inserted before the first component
private class WrappedFlowLayout : FlowLayout(LEADING, 0, DEFAULT_VGAP) {
override fun preferredLayoutSize(target: Container): Dimension {
val baseSize = super.preferredLayoutSize(target)
if (alignOnBaseline) return baseSize
return getWrappedSize(target)
}
private fun getWrappedSize(target: Container): Dimension {
val parent = SwingUtilities.getUnwrappedParent(target)
val maxWidth = parent.width - (parent.insets.left + parent.insets.right)
return getDimension(target, maxWidth)
}
private fun getDimension(target: Container, maxWidth: Int): Dimension {
val insets = target.insets
var height = insets.top + insets.bottom
var width = insets.left + insets.right
var rowHeight = 0
var rowWidth = insets.left + insets.right
var isVisible = false
var start = true
synchronized(target.treeLock) {
for (i in 0 until target.componentCount) {
val component = target.getComponent(i)
if (component.isVisible) {
isVisible = true
val size = component.preferredSize
if (rowWidth + hgap + size.width > maxWidth && !start) {
height += vgap + rowHeight
width = max(width, rowWidth)
rowWidth = insets.left + insets.right
rowHeight = 0
}
rowWidth += hgap + size.width
rowHeight = max(rowHeight, size.height)
start = false
}
}
height += vgap + rowHeight
width = max(width, rowWidth)
if (!isVisible) {
return super.preferredLayoutSize(target)
}
return Dimension(width, height)
}
}
override fun minimumLayoutSize(target: Container): Dimension {
return if (alignOnBaseline) super.minimumLayoutSize(target) else getWrappedSize(target)
}
}
}

View File

@@ -0,0 +1,81 @@
package com.intellij.ide.starters.shared.ui
import com.intellij.ide.starters.JavaStartersBundle
import com.intellij.ide.starters.remote.DependencyState
import com.intellij.ide.starters.remote.DependencyUnavailable
import com.intellij.ide.starters.shared.LibraryInfo
import com.intellij.icons.AllIcons.Actions
import com.intellij.openapi.roots.ui.componentsList.components.ScrollablePanel
import com.intellij.openapi.ui.popup.IconButton
import com.intellij.ui.InplaceButton
import com.intellij.ui.JBColor
import com.intellij.ui.ScrollPaneFactory
import com.intellij.ui.components.JBPanelWithEmptyText
import com.intellij.ui.components.panels.VerticalLayout
import com.intellij.ui.scale.JBUIScale
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.UIUtil
import com.intellij.util.ui.components.BorderLayoutPanel
import java.awt.BorderLayout
import java.awt.Cursor
import javax.swing.JLabel
internal class SelectedLibrariesPanel : JBPanelWithEmptyText(BorderLayout()) {
private val scrollablePanel: ScrollablePanel = ScrollablePanel(VerticalLayout(UIUtil.DEFAULT_VGAP))
private val scrollPane = ScrollPaneFactory.createScrollPane(scrollablePanel, true)
var libraryRemoveListener: ((LibraryInfo) -> Unit)? = null
var dependencyStateFunction: ((LibraryInfo) -> DependencyState)? = null
init {
this.background = UIUtil.getListBackground()
this.border = JBUI.Borders.customLine(JBColor.border(), 1)
add(scrollPane, BorderLayout.CENTER)
scrollablePanel.border = JBUI.Borders.empty(5)
scrollablePanel.background = UIUtil.getListBackground()
scrollPane.isVisible = false
}
fun update(libraries: Collection<LibraryInfo>) {
scrollablePanel.removeAll()
for (library in libraries) {
if (library.isRequired) continue // required are not shown
val dependencyPanel = BorderLayoutPanel()
dependencyPanel.background = UIUtil.getListBackground()
val dependencyLabel = JLabel(library.title)
dependencyLabel.border = JBUI.Borders.empty(0, UIUtil.DEFAULT_HGAP / 2, UIUtil.DEFAULT_VGAP, 0)
val dependencyStateFunction = this.dependencyStateFunction
if (dependencyStateFunction != null) {
val state = dependencyStateFunction.invoke(library)
if (state is DependencyUnavailable) {
dependencyLabel.isEnabled = false
dependencyLabel.toolTipText = state.message
}
}
val removeButton = InplaceButton(IconButton(
JavaStartersBundle.message("button.tooltip.remove"),
Actions.Close, Actions.CloseHovered)) {
libraryRemoveListener?.invoke(library)
}
removeButton.setTransform(0, -JBUIScale.scale(2.coerceAtLeast(dependencyLabel.font.size / 15)))
removeButton.cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
dependencyPanel.addToLeft(removeButton)
dependencyPanel.addToCenter(dependencyLabel)
scrollablePanel.add(dependencyPanel)
}
scrollPane.isVisible = scrollablePanel.componentCount > 0
scrollablePanel.revalidate()
scrollPane.revalidate()
revalidate()
}
}

View File

@@ -3,6 +3,7 @@ package com.intellij.openapi.roots.ui.configuration
import com.intellij.CommonBundle
import com.intellij.ide.JavaUiBundle
import com.intellij.ide.starters.JavaStartersBundle
import com.intellij.ide.util.PropertiesComponent
import com.intellij.ide.util.projectWizard.ModuleBuilder
import com.intellij.ide.util.projectWizard.ProjectWizardUtil
@@ -87,11 +88,11 @@ fun validateJavaVersion(sdkProperty: GraphProperty<Sdk?>, javaVersion: String?):
if (wizardVersion != null && javaVersion != null) {
val selectedVersion = JavaSdkVersion.fromVersionString(javaVersion)
if (selectedVersion != null && !wizardVersion.isAtLeast(selectedVersion)) {
Messages.showErrorDialog(JavaUiBundle.message("message.java.version.not.supported.by.sdk",
selectedVersion.description,
sdk.name,
wizardVersion.description),
JavaUiBundle.message("message.title.error"))
Messages.showErrorDialog(JavaStartersBundle.message("message.java.version.not.supported.by.sdk",
selectedVersion.description,
sdk.name,
wizardVersion.description),
JavaStartersBundle.message("message.title.error"))
return false
}
}

View File

@@ -0,0 +1,166 @@
// Copyright 2000-2021 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.
package com.intellij.ide.starters;
import com.intellij.ide.starters.shared.TextValidationFunction;
import com.intellij.ide.starters.shared.ValidationFunctions;
import com.intellij.openapi.util.Pair;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;
import java.util.Arrays;
import java.util.Collection;
import static org.junit.Assert.fail;
@RunWith(Parameterized.class)
public class ValidationFunctionsTest {
private static final String SHOULD_BE_VALID = "should be valid";
private static final Pair<TextValidationFunction, String> VERSION_FORMAT =
new Pair<>(ValidationFunctions.CHECK_SIMPLE_NAME_FORMAT, "version format");
private static final Pair<TextValidationFunction, String> NO_RESERVED_WORDS =
new Pair<>(ValidationFunctions.CHECK_NO_RESERVED_WORDS, "no reserved words");
private static final Pair<TextValidationFunction, String> GROUP_VALIDATOR =
new Pair<>(ValidationFunctions.CHECK_GROUP_FORMAT, "group format");
private static final Pair<TextValidationFunction, String> ARTIFACT_SIMPLE_VALIDATOR =
new Pair<>(ValidationFunctions.CHECK_ARTIFACT_SIMPLE_FORMAT, "artifact simple format");
private static final Pair<TextValidationFunction, String> ARTIFACT_WEB_VALIDATOR =
new Pair<>(ValidationFunctions.CHECK_ARTIFACT_FORMAT_FOR_WEB, "artifact web format");
@Parameter
public Pair<TextValidationFunction, String> validatorWithId;
@Parameter(1)
public String inputString;
@Parameter(2)
public boolean shouldPassValidation;
@Parameter(3)
public String comment;
@Parameters
@SuppressWarnings("SpellCheckingInspection")
public static Collection<Object[]> data() {
return Arrays.asList(new Object[][]{
{VERSION_FORMAT, "aAbBcC xXyYzZ-._", true, "all allowed symbols"},
{VERSION_FORMAT, "aaa * aaa", false, "*"},
{VERSION_FORMAT, "aaa , aaa", false, ","},
{VERSION_FORMAT, "aaa < aaa", false, "<"},
{VERSION_FORMAT, "aaa & aaa", false, "&"},
{VERSION_FORMAT, "aaa = aaa", false, "="},
{NO_RESERVED_WORDS, "e", true, SHOULD_BE_VALID},
{NO_RESERVED_WORDS, "example", true, SHOULD_BE_VALID},
{NO_RESERVED_WORDS, "com.example", true, SHOULD_BE_VALID},
{NO_RESERVED_WORDS, "com", true, SHOULD_BE_VALID},
{NO_RESERVED_WORDS, "com10", true, SHOULD_BE_VALID},
{NO_RESERVED_WORDS, "com9", false, "com9"},
{NO_RESERVED_WORDS, "com1", false, "com1"},
{NO_RESERVED_WORDS, "lpt", true, SHOULD_BE_VALID},
{NO_RESERVED_WORDS, "lpt10", true, SHOULD_BE_VALID},
{NO_RESERVED_WORDS, "lpt9", false, "lpt9"},
{NO_RESERVED_WORDS, "lpt1", false, "lpt1"},
{NO_RESERVED_WORDS, "con", false, "con"},
{NO_RESERVED_WORDS, "prn", false, "prn"},
{NO_RESERVED_WORDS, "aux", false, "aux"},
{NO_RESERVED_WORDS, "nul", false, "nul"},
{NO_RESERVED_WORDS, ".com1", false, "com1"},
{NO_RESERVED_WORDS, ".com1.", false, "com1"},
{NO_RESERVED_WORDS, ".com1&.", true, SHOULD_BE_VALID},
{NO_RESERVED_WORDS, "com1.", false, "com1"},
{NO_RESERVED_WORDS, " com1", false, "com1"},
{NO_RESERVED_WORDS, " com1 ", false, "com1"},
{NO_RESERVED_WORDS, "com1 ", false, "com1"},
{NO_RESERVED_WORDS, "com1.allowedword", false, "in the beginning"},
{NO_RESERVED_WORDS, "allowedword.com1", false, "in the end"},
{NO_RESERVED_WORDS, "allowedword.com1.allowedword", false, "in the middle"},
{NO_RESERVED_WORDS, "Com1", false, "com1 capitalized"},
{NO_RESERVED_WORDS, "coM1", false, "com1 capitalized"},
{NO_RESERVED_WORDS, "COM1", false, "com1 capitalized"},
{GROUP_VALIDATOR, "e", true, SHOULD_BE_VALID},
{GROUP_VALIDATOR, "example", true, SHOULD_BE_VALID},
{GROUP_VALIDATOR, "com.example", true, SHOULD_BE_VALID},
{GROUP_VALIDATOR, "aAzZ09_.aAzZ09_.aAzZ09_", true, "all allowed symbols"},
{GROUP_VALIDATOR, "looooooooooooooooooooooooooooooooooong.grouuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuup", true, SHOULD_BE_VALID},
{GROUP_VALIDATOR, "com.example ", false, "whitespace in the end"},
{GROUP_VALIDATOR, " com.example", false, "whitespace in the beginning"},
{GROUP_VALIDATOR, "com..example ", false, ".."},
{GROUP_VALIDATOR, "1com.example ", false, "digit at start of the first part"},
{GROUP_VALIDATOR, "com.1example ", false, "digit at start of the second part"},
{GROUP_VALIDATOR, "1com.1example ", false, "digits at start of second parts"},
{GROUP_VALIDATOR, "Aaaa aaaA", false, "forbidden symbol ' '"},
{GROUP_VALIDATOR, "Aaaa*aaaA", false, "forbidden symbol '*'"},
{GROUP_VALIDATOR, "Aaaa&aaaA", false, "forbidden symbol '&'"},
{GROUP_VALIDATOR, "Aaaa%aaaA", false, "forbidden symbol '%'"},
{ARTIFACT_SIMPLE_VALIDATOR, "e", true, SHOULD_BE_VALID},
{ARTIFACT_SIMPLE_VALIDATOR, "E", true, SHOULD_BE_VALID},
{ARTIFACT_SIMPLE_VALIDATOR, "example", true, SHOULD_BE_VALID},
{ARTIFACT_SIMPLE_VALIDATOR, "Example", true, SHOULD_BE_VALID},
{ARTIFACT_SIMPLE_VALIDATOR, "EXAMPLE", true, SHOULD_BE_VALID},
{ARTIFACT_SIMPLE_VALIDATOR, "_example", true, SHOULD_BE_VALID},
{ARTIFACT_SIMPLE_VALIDATOR, "azAZ09-_", true, "all allowed symbols"},
{ARTIFACT_SIMPLE_VALIDATOR, "1example", false, "digit at start"},
{ARTIFACT_SIMPLE_VALIDATOR, "example.", false, "dot at end"},
{ARTIFACT_SIMPLE_VALIDATOR, "example.example", false, "dot at center"},
{ARTIFACT_SIMPLE_VALIDATOR, ".example", false, "dot at start"},
{ARTIFACT_SIMPLE_VALIDATOR, "-example", false, "'-' at start"},
{ARTIFACT_SIMPLE_VALIDATOR, "Aaaa aaaA", false, "forbidden symbol ' '"},
{ARTIFACT_SIMPLE_VALIDATOR, "Aaaa*aaaA", false, "forbidden symbol '*'"},
{ARTIFACT_SIMPLE_VALIDATOR, "Aaaa&aaaA", false, "forbidden symbol '&'"},
{ARTIFACT_SIMPLE_VALIDATOR, "Aaaa%aaaA", false, "forbidden symbol '%'"},
{ARTIFACT_WEB_VALIDATOR, "e", true, SHOULD_BE_VALID},
{ARTIFACT_WEB_VALIDATOR, "example", true, SHOULD_BE_VALID},
{ARTIFACT_WEB_VALIDATOR, "example.", true, SHOULD_BE_VALID},
{ARTIFACT_WEB_VALIDATOR, "example.example", true, SHOULD_BE_VALID},
{ARTIFACT_WEB_VALIDATOR, "az09-._", true, "all allowed symbols"},
{ARTIFACT_WEB_VALIDATOR, "Example", false, "uppercase at start"},
{ARTIFACT_WEB_VALIDATOR, "examplE", false, "uppercase"},
{ARTIFACT_WEB_VALIDATOR, "1example", false, "digit at start"},
{ARTIFACT_WEB_VALIDATOR, ".example", false, "dot at start"},
{ARTIFACT_WEB_VALIDATOR, "-example", false, "'-' at start"},
{ARTIFACT_WEB_VALIDATOR, "_example", false, "'_' at start"},
{ARTIFACT_WEB_VALIDATOR, "Aaaa aaaA", false, "forbidden symbol ' '"},
{ARTIFACT_WEB_VALIDATOR, "Aaaa*aaaA", false, "forbidden symbol '*'"},
{ARTIFACT_WEB_VALIDATOR, "Aaaa&aaaA", false, "forbidden symbol '&'"},
{ARTIFACT_WEB_VALIDATOR, "Aaaa%aaaA", false, "forbidden symbol '%'"},
});
}
@Test
public void test() {
doTest(validatorWithId, inputString, shouldPassValidation, comment);
}
public static void doTest(Pair<TextValidationFunction, String> validatorWithId,
String inputString,
boolean shouldPassValidation, String comment) {
String validationErrorMessage = validatorWithId.first.checkText(inputString);
if (shouldPassValidation != (validationErrorMessage == null)) {
StringBuilder testReport = new StringBuilder();
testReport.append("Input string: \"").append(inputString).append("\"")
.append("\nValidator: ").append(validatorWithId.second)
.append("\nExpected validation result: ").append(shouldPassValidation)
.append("\nCurrent validation result: ").append(validationErrorMessage == null);
if (validationErrorMessage != null) {
testReport.append("\nValidation error message: ").append(validationErrorMessage);
}
if (comment != null) {
testReport.append("\nTest comment: ").append(comment);
}
fail(testReport.toString());
}
}
}

View File

@@ -267,6 +267,8 @@
<!-- See com.intellij.codeInspection.javaDoc.JavaDocLocalInspection -->
<extensionPoint qualifiedName="com.intellij.javaDocNotNecessary" interface="com.intellij.openapi.util.Condition" dynamic="true"/>
<extensionPoint qualifiedName="com.intellij.starter.moduleImporter" interface="com.intellij.ide.starters.StarterModuleImporter" dynamic="true"/>
<extensionPoint qualifiedName="com.intellij.starter.modulePreprocessor" interface="com.intellij.ide.starters.StarterModulePreprocessor" dynamic="true"/>
</extensionPoints>
<extensions defaultExtensionNs="com.intellij">
@@ -2144,6 +2146,10 @@
<newProjectWizard implementation="com.intellij.ide.projectWizard.generators.JavaNewProjectWizard" order="first"/>
<newProjectWizard implementation="com.intellij.ide.projectWizard.generators.KotlinNewProjectWizard"/>
<registryKey key="starters.dependency.update.host"
defaultValue="https://frameworks.jetbrains.com"
description="The host is used to download dependency config updates for framework starters"/>
</extensions>
<extensions defaultExtensionNs="org.jetbrains">

View File

@@ -86,5 +86,7 @@
<externalProjectDataService implementation="org.jetbrains.plugins.gradle.service.project.data.ExternalAnnotationsModuleLibrariesService"/>
<externalProjectDataService implementation="org.jetbrains.plugins.gradle.service.project.data.MavenRepositoriesDataService"/>
<externalProjectDataService implementation="org.jetbrains.plugins.gradle.service.project.data.AnnotationProcessingDataService"/>
<starter.moduleImporter implementation="org.jetbrains.plugins.gradle.starters.GradleStarterModuleImporter"/>
</extensions>
</idea-plugin>

View File

@@ -0,0 +1,46 @@
// Copyright 2000-2021 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.
package org.jetbrains.plugins.gradle.starters
import com.intellij.ide.starters.StarterModuleImporter
import com.intellij.openapi.module.Module
import com.intellij.openapi.roots.ModuleRootManager
import com.intellij.openapi.vfs.VfsUtilCore
import org.jetbrains.plugins.gradle.service.project.open.canLinkAndRefreshGradleProject
import org.jetbrains.plugins.gradle.service.project.open.linkAndRefreshGradleProject
import org.jetbrains.plugins.gradle.util.GradleConstants
import java.io.File
internal class GradleStarterModuleImporter : StarterModuleImporter {
override val id: String = "gradle"
override val title: String = "Gradle"
override fun runAfterSetup(module: Module): Boolean {
val project = module.project
val gradleFile = findGradleFile(module) ?: return true
val rootDirectory = gradleFile.parent
fixGradlewExecutableFlag(gradleFile.parentFile)
if (!canLinkAndRefreshGradleProject(rootDirectory, project)) return false
linkAndRefreshGradleProject(rootDirectory, project)
return false
}
private fun findGradleFile(module: Module): File? {
for (contentRoot in ModuleRootManager.getInstance(module).contentRoots) {
val baseDir = VfsUtilCore.virtualToIoFile(contentRoot)
var file = File(baseDir, GradleConstants.DEFAULT_SCRIPT_NAME)
if (file.exists()) return file
file = File(baseDir, GradleConstants.KOTLIN_DSL_SCRIPT_NAME)
if (file.exists()) return file
}
return null
}
private fun fixGradlewExecutableFlag(containingDir: File) {
val toFix = File(containingDir, "gradlew")
if (toFix.exists()) {
toFix.setExecutable(true, false)
}
}
}

View File

@@ -0,0 +1,48 @@
// Copyright 2000-2021 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.
package org.jetbrains.idea.maven.starters
import com.intellij.ide.starters.StarterModuleImporter
import com.intellij.openapi.module.Module
import com.intellij.openapi.roots.ModuleRootManager
import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.idea.maven.project.MavenProjectsManager
internal class MavenStarterModuleImporter : StarterModuleImporter {
override val id: String = "maven"
override val title: String = "Maven"
override fun runAfterSetup(module: Module): Boolean {
val project = module.project
val pomXMLs = mutableListOf<VirtualFile>()
for (contentRoot in ModuleRootManager.getInstance(module).contentRoots) {
collectPomXml(contentRoot, pomXMLs)
if (pomXMLs.isNotEmpty()) break
}
if (pomXMLs.isEmpty()) {
for (contentRoot in ModuleRootManager.getInstance(module).contentRoots) {
for (child in contentRoot.children) {
if (child.isDirectory) {
collectPomXml(child, pomXMLs)
}
}
}
}
if (pomXMLs.isEmpty()) {
return true
}
val mavenProjectsManager = MavenProjectsManager.getInstance(project)
mavenProjectsManager.addManagedFiles(pomXMLs)
return false
}
private fun collectPomXml(directoryFrom: VirtualFile, collectionInto: MutableCollection<VirtualFile>) {
val child = directoryFrom.findChild("pom.xml")
if (child != null) {
collectionInto.add(child)
}
}
}

View File

@@ -288,6 +288,8 @@
<toolWindowAllowlist id="Maven"/>
<compiler.buildIssueContributor implementation="org.jetbrains.idea.maven.externalSystemIntegration.output.quickfixes.JpsReleaseVersionQuickFix" />
<untrustedModeProvider implementation="org.jetbrains.idea.maven.project.MavenUntrustedModeProvider" />
<starters.moduleImporter implementation="org.jetbrains.idea.maven.starters.MavenStarterModuleImporter"/>
</extensions>
<extensions defaultExtensionNs="com.intellij.properties">