mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-03-22 15:19:59 +07:00
Frameworks: move starters APIs for New Project wizards to Java plugin
GitOrigin-RevId: e78209d9faf420b38a9d695d2653bfaae43d146c
This commit is contained in:
committed by
intellij-monorepo-bot
parent
8be8af8a94
commit
e898ecf93f
1
java/idea-ui/.gitattributes
vendored
Normal file
1
java/idea-ui/.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*.bin binary
|
||||
@@ -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>
|
||||
BIN
java/idea-ui/resources/assets/gradlew/gradle-wrapper.jar.bin
Normal file
BIN
java/idea-ui/resources/assets/gradlew/gradle-wrapper.jar.bin
Normal file
Binary file not shown.
89
java/idea-ui/resources/assets/gradlew/gradlew.bat.bin
Normal file
89
java/idea-ui/resources/assets/gradlew/gradlew.bat.bin
Normal 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
|
||||
185
java/idea-ui/resources/assets/gradlew/gradlew.bin
Normal file
185
java/idea-ui/resources/assets/gradlew/gradlew.bin
Normal 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" "$@"
|
||||
@@ -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?
|
||||
@@ -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})
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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?)
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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}"
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
119
java/idea-ui/src/com/intellij/ide/starters/local/StarterUtils.kt
Normal file
119
java/idea-ui/src/com/intellij/ide/starters/local/StarterUtils.kt
Normal 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
|
||||
}
|
||||
}
|
||||
74
java/idea-ui/src/com/intellij/ide/starters/local/Starters.kt
Normal file
74
java/idea-ui/src/com/intellij/ide/starters/local/Starters.kt
Normal 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
|
||||
)
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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")!!)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
@@ -0,0 +1,4 @@
|
||||
@ApiStatus.Experimental
|
||||
package com.intellij.ide.starters.remote;
|
||||
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
218
java/idea-ui/src/com/intellij/ide/starters/shared/FormUiUtil.kt
Normal file
218
java/idea-ui/src/com/intellij/ide/starters/shared/FormUiUtil.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
@ApiStatus.Experimental
|
||||
package com.intellij.ide.starters.shared;
|
||||
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user