mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
jdks - move implementation to the intellij.java.ui module, cleanup code, update JSON data format, add more tests
IDEA-225308 GitOrigin-RevId: 54f0428b4b5350b457c7ac37fa5c37f4681e41b6
This commit is contained in:
committed by
intellij-monorepo-bot
parent
f581a02207
commit
b1f5fce9ed
@@ -18,5 +18,9 @@
|
||||
<orderEntry type="library" name="jna" level="project" />
|
||||
<orderEntry type="module" module-name="intellij.platform.externalSystem" />
|
||||
<orderEntry type="module" module-name="intellij.platform.externalSystem.impl" />
|
||||
<orderEntry type="library" name="jackson" level="project" />
|
||||
<orderEntry type="library" name="jackson-databind" level="project" />
|
||||
<orderEntry type="library" name="jackson-module-kotlin" level="project" />
|
||||
<orderEntry type="library" name="xz" level="project" />
|
||||
</component>
|
||||
</module>
|
||||
@@ -0,0 +1,237 @@
|
||||
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
|
||||
package com.intellij.jdkDownloader
|
||||
|
||||
import com.intellij.ide.DataManager
|
||||
import com.intellij.openapi.actionSystem.CommonDataKeys
|
||||
import com.intellij.openapi.application.invokeLater
|
||||
import com.intellij.openapi.diagnostic.logger
|
||||
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
|
||||
import com.intellij.openapi.progress.ProcessCanceledException
|
||||
import com.intellij.openapi.progress.ProgressIndicator
|
||||
import com.intellij.openapi.progress.ProgressManager
|
||||
import com.intellij.openapi.progress.Task
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.projectRoots.SdkModel
|
||||
import com.intellij.openapi.projectRoots.SdkType
|
||||
import com.intellij.openapi.projectRoots.impl.JDKDownloaderService
|
||||
import com.intellij.openapi.projectRoots.impl.JavaSdkImpl
|
||||
import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil
|
||||
import com.intellij.openapi.roots.ui.configuration.projectRoot.ProjectSdksModel
|
||||
import com.intellij.openapi.ui.*
|
||||
import com.intellij.openapi.util.SystemInfo
|
||||
import com.intellij.ui.DocumentAdapter
|
||||
import com.intellij.ui.components.textFieldWithBrowseButton
|
||||
import com.intellij.ui.layout.*
|
||||
import com.intellij.util.Consumer
|
||||
import java.awt.Component
|
||||
import java.awt.event.ActionEvent
|
||||
import java.awt.event.ItemEvent
|
||||
import java.util.function.Supplier
|
||||
import javax.swing.DefaultComboBoxModel
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.event.DocumentEvent
|
||||
import javax.swing.event.DocumentListener
|
||||
|
||||
private const val DIALOG_TITLE = "Add new JDK"
|
||||
|
||||
internal class JDKDownloaderServiceUI : JDKDownloaderService() {
|
||||
private val LOG = logger<JDKDownloaderService>()
|
||||
|
||||
override fun downloadOrSelectJDK(javaSdkType: JavaSdkImpl,
|
||||
sdkModel: SdkModel,
|
||||
parentComponent: JComponent,
|
||||
callback: Consumer<Sdk>) {
|
||||
val project = CommonDataKeys.PROJECT.getData(DataManager.getInstance().getDataContext(parentComponent)) ?: return
|
||||
|
||||
fun addSdkIfNotNull(jdkHome: String?) {
|
||||
if (jdkHome == null) return
|
||||
val sdk = (sdkModel as ProjectSdksModel).createSdk(javaSdkType, jdkHome)
|
||||
callback.consume(sdk)
|
||||
}
|
||||
|
||||
ProgressManager.getInstance().run(object : Task.Modal(project, "Downloading JDK list...", true) {
|
||||
override fun run(indicator: ProgressIndicator) {
|
||||
val items = try {
|
||||
JDKListDownloader.downloadModel(progress = indicator)
|
||||
}
|
||||
catch (t: ProcessCanceledException) {
|
||||
return
|
||||
}
|
||||
catch (t: Exception) {
|
||||
LOG.warn(t.message, t)
|
||||
Messages.showMessageDialog(parentComponent,
|
||||
"Failed to download the list of installable JDKs. You could still locate installed JDK in the disk",
|
||||
DIALOG_TITLE,
|
||||
Messages.getErrorIcon()
|
||||
)
|
||||
|
||||
val jdkHome = showJDKSelectorFromDisk(javaSdkType, project, parentComponent)
|
||||
addSdkIfNotNull(jdkHome)
|
||||
return
|
||||
}
|
||||
|
||||
invokeLater {
|
||||
if (project.isDisposedOrDisposeInProgress) return@invokeLater
|
||||
val jdkHome = SelectOrDownloadJDKDialog(project, parentComponent, javaSdkType, items).selectOrDownloadAndUnpackJDK()
|
||||
addSdkIfNotNull(jdkHome)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun showJDKSelectorFromDisk(sdkType: SdkType, project: Project?, component: Component?): String? {
|
||||
var jdkHome: String? = null
|
||||
SdkConfigurationUtil.selectSdkHome(sdkType, project, component) {
|
||||
jdkHome = it
|
||||
}
|
||||
return jdkHome
|
||||
}
|
||||
|
||||
private class SelectOrDownloadJDKDialog(
|
||||
val project: Project,
|
||||
val parentComponent: Component?,
|
||||
val sdkType: SdkType,
|
||||
val items: List<JDKItem>
|
||||
) : DialogWrapper(project, parentComponent, false, IdeModalityType.PROJECT) {
|
||||
private val LOG = logger<SelectOrDownloadJDKDialog>()
|
||||
|
||||
private val panel: JComponent
|
||||
|
||||
private val selectFromDiskAction = object : DialogWrapperAction("Find on the disk...") {
|
||||
override fun doAction(e: ActionEvent?) = doSelectFromDiskAction()
|
||||
}
|
||||
|
||||
private lateinit var selectedItem: JDKItem
|
||||
private lateinit var selectedPath: String
|
||||
|
||||
private lateinit var resultingJDKHome: String
|
||||
|
||||
private lateinit var installDirTextField: TextFieldWithBrowseButton
|
||||
|
||||
init {
|
||||
title = DIALOG_TITLE
|
||||
setResizable(false)
|
||||
|
||||
val defaultItem = items.singleOrNull { it.isDefaultItem } ?: items.minBy { it.product } ?: error("There must be at least one item")
|
||||
|
||||
val vendorComboBox = ComboBox(items.map { it.product }.distinct().sorted().toTypedArray())
|
||||
vendorComboBox.selectedItem = defaultItem.product
|
||||
vendorComboBox.renderer = listCellRenderer { it, _, _ -> setText(it.getPackagePresentationText) }
|
||||
|
||||
val versionModel = DefaultComboBoxModel<JDKItem>()
|
||||
val versionComboBox = ComboBox(versionModel)
|
||||
versionComboBox.renderer = listCellRenderer { it, _, _ -> setText(it.getVersionPresentationText) }
|
||||
|
||||
fun selectVersions(newProduct: JDKProduct) {
|
||||
val newVersions = items.filter { it.product == newProduct }.sorted()
|
||||
versionModel.removeAllElements()
|
||||
for (version in newVersions) {
|
||||
versionModel.addElement(version)
|
||||
}
|
||||
}
|
||||
|
||||
installDirTextField = textFieldWithBrowseButton(
|
||||
project = project,
|
||||
browseDialogTitle = "Select installation path for the JDK",
|
||||
fileChooserDescriptor = FileChooserDescriptorFactory.createSingleFolderDescriptor()
|
||||
)
|
||||
|
||||
fun selectInstallPath(newVersion: JDKItem) {
|
||||
val installFolderName = newVersion.installFolderName
|
||||
val path = when {
|
||||
SystemInfo.isLinux || SystemInfo.isMac -> "~/.jdks/$installFolderName"
|
||||
SystemInfo.isWindows -> "${System.getProperty("user.home")}\\.jdks\\${installFolderName}"
|
||||
else -> error("Unsupported OS")
|
||||
}
|
||||
installDirTextField.text = path
|
||||
selectedItem = newVersion
|
||||
}
|
||||
|
||||
vendorComboBox.onSelectionChange(::selectVersions)
|
||||
versionComboBox.onSelectionChange(::selectInstallPath)
|
||||
installDirTextField.onTextChange {
|
||||
selectedPath = it
|
||||
}
|
||||
|
||||
panel = panel {
|
||||
row("Vendor:") { vendorComboBox.invoke() }
|
||||
row("Version:") { versionComboBox.invoke() }
|
||||
row("Install JDK to:") { installDirTextField.invoke() }
|
||||
}
|
||||
|
||||
init()
|
||||
selectVersions(defaultItem.product)
|
||||
}
|
||||
|
||||
override fun doValidate(): ValidationInfo? {
|
||||
super.doValidate()?.let { return it }
|
||||
|
||||
val path = selectedPath
|
||||
val (_, error) = JDKInstaller.validateInstallDir(path)
|
||||
return error?.let { ValidationInfo(error, installDirTextField) }
|
||||
}
|
||||
|
||||
override fun createActions() = arrayOf(selectFromDiskAction, *super.createActions())
|
||||
override fun createCenterPanel() = panel
|
||||
|
||||
private fun doSelectFromDiskAction() {
|
||||
val jdkHome = showJDKSelectorFromDisk(sdkType, project, panel)
|
||||
if (jdkHome != null) {
|
||||
resultingJDKHome = jdkHome
|
||||
close(OK_EXIT_CODE)
|
||||
}
|
||||
}
|
||||
|
||||
override fun doOKAction() {
|
||||
val installItem = selectedItem
|
||||
val installPath = selectedPath
|
||||
|
||||
ProgressManager.getInstance().run(object : Task.Modal(project, "Installing JDK...", true) {
|
||||
override fun run(indicator: ProgressIndicator) {
|
||||
try {
|
||||
val targetDir = JDKInstaller.installJDK(installItem, installPath, indicator)
|
||||
invokeLater {
|
||||
resultingJDKHome = targetDir.absolutePath
|
||||
superDoOKAction()
|
||||
}
|
||||
} catch (t: ProcessCanceledException) {
|
||||
return
|
||||
} catch (e: Exception) {
|
||||
LOG.warn("Failed to install JDK $installItem to $installPath. ${e.message}", e)
|
||||
invokeLater {
|
||||
Messages.showMessageDialog(panel,
|
||||
"Failed to install JDK. ${e.message}",
|
||||
DIALOG_TITLE,
|
||||
Messages.getErrorIcon()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun superDoOKAction() = super.doOKAction()
|
||||
|
||||
// returns unpacked JDK location (if any) or null if cancelled
|
||||
fun selectOrDownloadAndUnpackJDK(): String? = when {
|
||||
showAndGet() -> resultingJDKHome
|
||||
else -> null
|
||||
}
|
||||
|
||||
private inline fun TextFieldWithBrowseButton.onTextChange(crossinline action: (String) -> Unit) {
|
||||
textField.document.addDocumentListener(object : DocumentAdapter() {
|
||||
override fun textChanged(e: DocumentEvent) {
|
||||
action(text)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private inline fun <reified T> ComboBox<T>.onSelectionChange(crossinline action: (T) -> Unit) {
|
||||
this.addItemListener { e ->
|
||||
if (e.stateChange == ItemEvent.SELECTED) action(e.item as T)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
|
||||
package com.intellij.jdkDownloader
|
||||
|
||||
import com.google.common.hash.Hashing
|
||||
import com.google.common.io.Files
|
||||
import com.intellij.openapi.application.PathManager
|
||||
import com.intellij.openapi.diagnostic.logger
|
||||
import com.intellij.openapi.progress.ProcessCanceledException
|
||||
import com.intellij.openapi.progress.ProgressIndicator
|
||||
import com.intellij.openapi.util.io.FileUtil
|
||||
import com.intellij.util.Urls
|
||||
import com.intellij.util.io.Decompressor
|
||||
import com.intellij.util.io.HttpRequests
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.lang.RuntimeException
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
object JDKInstaller {
|
||||
private val LOG = logger<JDKInstaller>()
|
||||
|
||||
fun validateInstallDir(selectedPath: String): Pair<File?, String?> {
|
||||
if (selectedPath.isBlank()) return null to "Target path is empty"
|
||||
|
||||
val targetDir = kotlin.runCatching { File(FileUtil.expandUserHome(selectedPath)) }.getOrElse { t ->
|
||||
LOG.warn("Failed to resolve user path: $selectedPath. ${t.message}", t)
|
||||
return null to (t.message ?: "Failed to resolve path")
|
||||
}
|
||||
|
||||
if (targetDir.isFile) return null to "Target path is an existing file"
|
||||
if (targetDir.isDirectory && targetDir.listFiles()?.isNotEmpty() == true) {
|
||||
return null to "Target path is an existing non-empty directory"
|
||||
}
|
||||
|
||||
return targetDir to null
|
||||
}
|
||||
|
||||
fun installJDK(item: JDKItem, selectedPath: String, indicator: ProgressIndicator?): File {
|
||||
indicator?.text = "Installing ${item.getFullPresentationText}..."
|
||||
|
||||
val (targetDir, error) = validateInstallDir(selectedPath)
|
||||
if (targetDir == null || error != null) throw RuntimeException(error ?: "Invalid Target Directory")
|
||||
|
||||
val url = Urls.parse(item.url, false) ?: error("Cannot parse download URL: ${item.url}")
|
||||
if (!url.scheme.equals("https", ignoreCase = true)) error("URL must use https:// protocol, but was: $url")
|
||||
|
||||
indicator?.text2 = "Downloading"
|
||||
val downloadPath = File(PathManager.getTempPath(), "jdk-${item.archiveFileName}")
|
||||
try {
|
||||
try {
|
||||
HttpRequests.request(item.url)
|
||||
.productNameAsUserAgent()
|
||||
.connect { processor -> processor.saveToFile(downloadPath, indicator) }
|
||||
|
||||
}
|
||||
catch (t: IOException) {
|
||||
throw RuntimeException("Failed to download JDK from $url. ${t.message}", t)
|
||||
}
|
||||
|
||||
val sizeDiff = downloadPath.length() - item.archiveSize
|
||||
if (sizeDiff != 0L) {
|
||||
throw RuntimeException("Downloaded JDK distribution has incorrect size, difference is ${sizeDiff.absoluteValue} bytes")
|
||||
}
|
||||
|
||||
val actualHashCode = Files.asByteSource(downloadPath).hash(Hashing.sha256()).toString()
|
||||
if (!actualHashCode.equals(item.sha256, ignoreCase = true)) {
|
||||
throw RuntimeException("SHA-256 checksum does not match. Actual value is $actualHashCode, expected ${item.sha256}")
|
||||
}
|
||||
|
||||
indicator?.isIndeterminate = true
|
||||
indicator?.text2 = "Unpacking"
|
||||
|
||||
val decompressor = when (item.packageType) {
|
||||
"zip" -> Decompressor.Zip(downloadPath)
|
||||
"targz" -> Decompressor.Tar(downloadPath).withSymlinks()
|
||||
else -> error("Unsupported archiveType: ${item.archiveSize}")
|
||||
}
|
||||
//handle cancellation via postProcessor (instead of inheritance)
|
||||
decompressor.cutDirs(item.unpackCutDirs)
|
||||
decompressor.postprocessor { indicator?.checkCanceled() }
|
||||
|
||||
|
||||
val fullMatchPath = item.unpackPrefixFilter.trim('/')
|
||||
|
||||
if (!fullMatchPath.isBlank()) {
|
||||
val baseMatchPath = item.unpackPrefixFilter.trim('/') + "/"
|
||||
decompressor.filter { entry ->
|
||||
indicator?.checkCanceled()
|
||||
if (entry.trim('/').equals(fullMatchPath, ignoreCase = true)) return@filter true
|
||||
if (entry.trimStart('/').startsWith(baseMatchPath, ignoreCase = true)) return@filter true
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
decompressor.extract(targetDir)
|
||||
}
|
||||
catch (t: Throwable) {
|
||||
//if we were cancelled in the middle or failed, let's clean up
|
||||
FileUtil.delete(targetDir)
|
||||
if (t is ProcessCanceledException) throw t
|
||||
if (t is IOException) throw RuntimeException("Failed to extract JDK package", t)
|
||||
throw t
|
||||
}
|
||||
finally {
|
||||
FileUtil.delete(downloadPath)
|
||||
}
|
||||
return targetDir
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
|
||||
package com.intellij.jdkDownloader
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode
|
||||
import com.intellij.openapi.progress.ProgressIndicator
|
||||
import com.intellij.openapi.util.SystemInfo
|
||||
import com.intellij.openapi.util.registry.Registry
|
||||
import com.intellij.openapi.util.text.StringUtil
|
||||
import com.intellij.util.io.HttpRequests
|
||||
import com.intellij.util.text.VersionComparatorUtil
|
||||
import org.tukaani.xz.XZInputStream
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.IOException
|
||||
import java.lang.RuntimeException
|
||||
|
||||
/** describes vendor + product part of the UI **/
|
||||
data class JDKProduct(
|
||||
private val vendor: String,
|
||||
private val product: String?,
|
||||
private val flavour: String?
|
||||
) : Comparable<JDKProduct> {
|
||||
private fun String?.compareToIgnoreCase(other: String?): Int {
|
||||
if (this == other) return 0
|
||||
if (this == null && other != null) return -1
|
||||
if (this != null && other == null) return 1
|
||||
if (this != null && other != null) return this.compareTo(other, ignoreCase = true)
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun compareTo(other: JDKProduct): Int {
|
||||
var cmp = this.vendor.compareToIgnoreCase(other.vendor)
|
||||
if (cmp != 0) return cmp
|
||||
cmp = this.product.compareToIgnoreCase(other.product)
|
||||
if (cmp != 0) return cmp
|
||||
return this.flavour.compareToIgnoreCase(other.flavour)
|
||||
}
|
||||
|
||||
val getPackagePresentationText : String get() = buildString {
|
||||
append(vendor)
|
||||
if (product != null) {
|
||||
append(" ")
|
||||
append(product)
|
||||
}
|
||||
|
||||
if (flavour != null) {
|
||||
append(" (")
|
||||
append(flavour)
|
||||
append(")")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/** describes an item behind the version as well as download info **/
|
||||
data class JDKItem(
|
||||
val product: JDKProduct,
|
||||
|
||||
val isDefaultItem: Boolean = false,
|
||||
|
||||
private val jdkMajorVersion: Int,
|
||||
private val jdkVersion: String,
|
||||
private val jdkVendorVersion: String?,
|
||||
private val vendorVersion: String?,
|
||||
|
||||
val arch: String,
|
||||
val packageType: String,
|
||||
val url: String,
|
||||
val sha256: String,
|
||||
|
||||
val archiveSize: Long,
|
||||
val unpackedSize: Long,
|
||||
|
||||
// normally archive container a root folder inside, or several for macOS bundles
|
||||
// we need to know how many to skip
|
||||
val unpackCutDirs: Int,
|
||||
// we should only extract items tarting from the given prefix (e.g. masOS bundle)
|
||||
val unpackPrefixFilter: String,
|
||||
|
||||
val archiveFileName: String,
|
||||
val installFolderName: String
|
||||
) : Comparable<JDKItem> {
|
||||
|
||||
override fun compareTo(other: JDKItem): Int {
|
||||
var cmp = -this.jdkMajorVersion.compareTo(other.jdkMajorVersion)
|
||||
if (cmp != 0) return cmp
|
||||
cmp = -VersionComparatorUtil.compare(this.jdkVersion, other.jdkVersion)
|
||||
if (cmp != 0) return cmp
|
||||
cmp = VersionComparatorUtil.compare(this.jdkVendorVersion, other.jdkVendorVersion)
|
||||
if (cmp != 0) return cmp
|
||||
return VersionComparatorUtil.compare(this.vendorVersion, other.vendorVersion)
|
||||
}
|
||||
|
||||
val getVersionPresentationText : String get() = buildString {
|
||||
append(jdkVersion)
|
||||
append(" (")
|
||||
append(StringUtil.formatFileSize(archiveSize))
|
||||
append(")")
|
||||
}
|
||||
|
||||
val getFullPresentationText : String get() = product.getPackagePresentationText + " " + getVersionPresentationText
|
||||
}
|
||||
|
||||
object JDKListDownloader {
|
||||
private val feedUrl: String
|
||||
get() {
|
||||
val registry = runCatching { Registry.get("jdk.downloader.url").asString() }.getOrNull()
|
||||
if (!registry.isNullOrBlank()) return registry
|
||||
|
||||
//TODO: let's use CDN URL in once it'd be established
|
||||
return "https://buildserver.labs.intellij.net/guestAuth/repository/download/ijplatform_master_Service_GenerateJDKsJson/lasest.lastSuccessful/feed.zip!/jdks.json.xz"
|
||||
}
|
||||
|
||||
fun downloadModel(progress: ProgressIndicator?, feedUrl: String = JDKListDownloader.feedUrl): List<JDKItem> {
|
||||
//we download XZ packed version of the data (several KBs packed, several dozen KBs unpacked) and process it in-memory
|
||||
val rawData = try {
|
||||
//timeouts are handled inside
|
||||
HttpRequests
|
||||
.request(feedUrl)
|
||||
.productNameAsUserAgent()
|
||||
.readBytes(progress)
|
||||
.unXZ()
|
||||
}
|
||||
catch (t: IOException) {
|
||||
throw RuntimeException("Failed to download and process the JDKs list from $feedUrl. ${t.message}", t)
|
||||
}
|
||||
|
||||
try {
|
||||
val tree = ObjectMapper().readTree(rawData) as? ObjectNode ?: error("Unexpected JSON data")
|
||||
val items = tree["jdks"] as? ArrayNode ?: error("`jdks` element is missing")
|
||||
|
||||
val expectedOS = when {
|
||||
SystemInfo.isWindows -> "windows"
|
||||
SystemInfo.isMac -> "macOS"
|
||||
SystemInfo.isLinux -> "linux"
|
||||
else -> error("Unsupported OS")
|
||||
}
|
||||
|
||||
val result = mutableListOf<JDKItem>()
|
||||
for (item in items.filterIsInstance<ObjectNode>()) {
|
||||
val packages = item["packages"] as? ArrayNode ?: continue
|
||||
val pkg = packages.filterIsInstance<ObjectNode>().singleOrNull { it["os"]?.asText() == expectedOS } ?: continue
|
||||
|
||||
val product = JDKProduct(
|
||||
vendor = item["vendor"]?.asText() ?: continue,
|
||||
product = item["product"]?.asText(),
|
||||
flavour = item["flavour"]?.asText()
|
||||
)
|
||||
|
||||
result += JDKItem(product = product,
|
||||
isDefaultItem = item["default"]?.asBoolean() ?: false,
|
||||
|
||||
jdkMajorVersion = item["jdk_version_major"]?.asInt() ?: continue,
|
||||
jdkVersion = item["jdk_version"]?.asText() ?: continue,
|
||||
jdkVendorVersion = item["jdk_vendor_version"]?.asText(),
|
||||
vendorVersion = item["vendor_version"]?.asText(),
|
||||
|
||||
arch = pkg["arch"]?.asText() ?: continue,
|
||||
packageType = pkg["package_type"]?.asText() ?: continue,
|
||||
url = pkg["url"]?.asText() ?: continue,
|
||||
sha256 = pkg["sha256"]?.asText() ?: continue,
|
||||
archiveSize = pkg["archive_size"]?.asLong() ?: continue,
|
||||
archiveFileName = pkg["archive_file_name"]?.asText() ?: continue,
|
||||
unpackCutDirs = pkg["unpack_cut_dirs"]?.asInt() ?: continue,
|
||||
unpackPrefixFilter = pkg["unpack_prefix_filter"]?.asText() ?: continue,
|
||||
|
||||
unpackedSize = pkg["unpacked_size"]?.asLong() ?: continue,
|
||||
installFolderName = pkg["install_folder_name"]?.asText() ?: continue
|
||||
)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
catch (t: Throwable) {
|
||||
throw RuntimeException("Failed to parse downloaded JDKs list from $feedUrl. ${t.message}", t)
|
||||
}
|
||||
}
|
||||
|
||||
private fun ByteArray.unXZ() = ByteArrayInputStream(this).use { input ->
|
||||
XZInputStream(input).use { it.readBytes() }
|
||||
}
|
||||
}
|
||||
@@ -68,10 +68,6 @@
|
||||
</library>
|
||||
</orderEntry>
|
||||
<orderEntry type="module" module-name="intellij.copyright" />
|
||||
<orderEntry type="library" name="jackson" level="project" />
|
||||
<orderEntry type="library" name="jackson-databind" level="project" />
|
||||
<orderEntry type="library" name="jackson-module-kotlin" level="project" />
|
||||
<orderEntry type="library" name="xz" level="project" />
|
||||
</component>
|
||||
<component name="copyright">
|
||||
<Base>
|
||||
|
||||
@@ -289,7 +289,8 @@
|
||||
|
||||
<sdkType implementation="com.intellij.openapi.projectRoots.impl.JavaSdkImpl"/>
|
||||
|
||||
<applicationService serviceImplementation="com.intellij.openapi.projectRoots.impl.JDKDownloaderService"/>
|
||||
<applicationService serviceInterface="com.intellij.openapi.projectRoots.impl.JDKDownloaderService"
|
||||
serviceImplementation="com.intellij.jdkDownloader.JDKDownloaderServiceUI"/>
|
||||
<registryKey key="jdk.downloader.ui" defaultValue="false" description="jdk.downloader.ui.description=Use the list of JDKs to download and install in one click"/>
|
||||
<registryKey key="jdk.downloader.url" description="Custom URL for JDKs list"/>
|
||||
|
||||
|
||||
@@ -1,291 +1,27 @@
|
||||
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
|
||||
package com.intellij.openapi.projectRoots.impl
|
||||
|
||||
import com.google.common.hash.Hashing
|
||||
import com.google.common.io.Files
|
||||
import com.intellij.ide.DataManager
|
||||
import com.intellij.openapi.actionSystem.CommonDataKeys
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.application.PathManager
|
||||
import com.intellij.openapi.application.invokeLater
|
||||
import com.intellij.openapi.diagnostic.logger
|
||||
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
|
||||
import com.intellij.openapi.progress.ProgressIndicator
|
||||
import com.intellij.openapi.progress.ProgressManager
|
||||
import com.intellij.openapi.progress.Task
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.projectRoots.SdkModel
|
||||
import com.intellij.openapi.projectRoots.SdkType
|
||||
import com.intellij.openapi.roots.ui.configuration.projectRoot.ProjectSdksModel
|
||||
import com.intellij.openapi.ui.ComboBox
|
||||
import com.intellij.openapi.ui.DialogWrapper
|
||||
import com.intellij.openapi.ui.TextFieldWithBrowseButton
|
||||
import com.intellij.openapi.util.SystemInfo
|
||||
import com.intellij.openapi.util.io.FileUtil
|
||||
import com.intellij.openapi.util.registry.Registry
|
||||
import com.intellij.openapi.util.text.StringUtil
|
||||
import com.intellij.ui.components.textFieldWithBrowseButton
|
||||
import com.intellij.ui.layout.*
|
||||
import com.intellij.util.Consumer
|
||||
import com.intellij.util.Urls
|
||||
import com.intellij.util.io.Decompressor
|
||||
import com.intellij.util.io.HttpRequests
|
||||
import java.awt.Component
|
||||
import java.awt.event.ActionEvent
|
||||
import java.awt.event.ItemEvent
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.lang.RuntimeException
|
||||
import javax.swing.DefaultComboBoxModel
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.event.DocumentEvent
|
||||
import javax.swing.event.DocumentListener
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
private val LOG = logger<JDKDownloaderService>()
|
||||
|
||||
internal class JDKDownloaderService {
|
||||
abstract class JDKDownloaderService {
|
||||
abstract fun downloadOrSelectJDK(javaSdkType: JavaSdkImpl,
|
||||
sdkModel: SdkModel,
|
||||
parentComponent: JComponent,
|
||||
callback: Consumer<Sdk>)
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun getInstance(): JDKDownloaderService? = if (!isEnabled) null else ApplicationManager.getApplication().getService(JDKDownloaderService::class.java)
|
||||
fun getInstanceIfEnabled(): JDKDownloaderService? = if (!Registry.`is`("jdk.downloader.ui")) null
|
||||
else ApplicationManager.getApplication().getService(JDKDownloaderService::class.java)
|
||||
|
||||
@JvmStatic
|
||||
val isEnabled
|
||||
get() = Registry.`is`("jdk.downloader.ui")
|
||||
}
|
||||
|
||||
fun showCustomCreateUI(javaSdkType: JavaSdkImpl,
|
||||
sdkModel: SdkModel,
|
||||
parentComponent: JComponent,
|
||||
callback: Consumer<Sdk>) {
|
||||
val project = CommonDataKeys.PROJECT.getData(DataManager.getInstance().getDataContext(parentComponent)) ?: return
|
||||
|
||||
ProgressManager.getInstance().run(object : Task.Modal(project, "Downloading JDK list...", true) {
|
||||
override fun run(indicator: ProgressIndicator) {
|
||||
val items = try {
|
||||
JDKListDownloader.downloadModel(progress = indicator)
|
||||
} catch (t: IOException) {
|
||||
LOG.warn(t.message, t)
|
||||
return
|
||||
}
|
||||
|
||||
invokeLater {
|
||||
if (project.isDisposedOrDisposeInProgress) return@invokeLater
|
||||
val jdkHome = SelectOrDownloadJDKDialog(project, parentComponent, javaSdkType, items).selectOrDownloadAndUnpackJDK()
|
||||
|
||||
if (jdkHome != null) {
|
||||
(sdkModel as ProjectSdksModel).addSdk(javaSdkType, jdkHome, callback)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private class SelectOrDownloadJDKDialog(
|
||||
val project: Project,
|
||||
val parentComponent: Component?,
|
||||
val sdkType: SdkType,
|
||||
val items: List<JDKDownloadItem>
|
||||
): DialogWrapper(project, parentComponent, false, IdeModalityType.PROJECT) {
|
||||
|
||||
private val panel : JComponent
|
||||
|
||||
private val selectFromDiskAction = object: DialogWrapperAction("Find on the disk...") {
|
||||
override fun doAction(e: ActionEvent?) = doSelectFromDiskAction()
|
||||
}
|
||||
|
||||
private lateinit var selectedItem: JDKDownloadItem
|
||||
private lateinit var selectedPath: String
|
||||
|
||||
private lateinit var resultingJDKHome: String
|
||||
|
||||
init {
|
||||
title = "Download JDK"
|
||||
setResizable(false)
|
||||
|
||||
val defaultItem = items.first()
|
||||
|
||||
val vendorComboBox = ComboBox(items.map { it.vendor }.distinct().sortedBy { it.vendor.toUpperCase() }.toTypedArray())
|
||||
vendorComboBox.selectedItem = defaultItem.vendor
|
||||
vendorComboBox.renderer = listCellRenderer { vendor, _, _ -> setText(vendor.vendor) }
|
||||
|
||||
val versionModel = DefaultComboBoxModel<JDKDownloadItem>()
|
||||
val versionComboBox = ComboBox(versionModel)
|
||||
versionComboBox.renderer = listCellRenderer { it, _, _ ->
|
||||
setText("${it.version} (${StringUtil.formatFileSize(it.size)})")
|
||||
}
|
||||
|
||||
fun selectVersions(newVendor: JDKVendor) {
|
||||
val newVersions = items.filter { it.vendor == newVendor }.sortedBy { it.version.toLowerCase() }
|
||||
versionModel.removeAllElements()
|
||||
for (version in newVersions) {
|
||||
versionModel.addElement(version)
|
||||
}
|
||||
}
|
||||
selectVersions(defaultItem.vendor)
|
||||
|
||||
val installDirTextField = textFieldWithBrowseButton(
|
||||
project = project,
|
||||
browseDialogTitle = "Select installation path for the JDK",
|
||||
fileChooserDescriptor = FileChooserDescriptorFactory.createSingleFolderDescriptor()
|
||||
)
|
||||
|
||||
fun selectInstallPath(newVersion: JDKDownloadItem) {
|
||||
val path = when {
|
||||
SystemInfo.isLinux || SystemInfo.isMac -> "~/.jdks/${newVersion.installFolderName}"
|
||||
SystemInfo.isWindows -> System.getProperty("user.home") + "\\.jdks\\${newVersion.installFolderName}"
|
||||
else -> error("Unsupported OS")
|
||||
}
|
||||
installDirTextField.text = path
|
||||
selectedPath = path
|
||||
selectedItem = newVersion
|
||||
}
|
||||
selectInstallPath(defaultItem)
|
||||
|
||||
vendorComboBox.onSelectionChange(::selectVersions)
|
||||
versionComboBox.onSelectionChange(::selectInstallPath)
|
||||
installDirTextField.onTextChange { selectedPath = it } //TODO: validate paths?
|
||||
|
||||
panel = panel {
|
||||
row("Vendor:") { vendorComboBox.invoke() }
|
||||
row("Version:") { versionComboBox.invoke() }
|
||||
row("Install JDK to:") { installDirTextField.invoke() }
|
||||
}
|
||||
|
||||
init()
|
||||
}
|
||||
|
||||
override fun createActions() = arrayOf(selectFromDiskAction, *super.createActions())
|
||||
override fun createCenterPanel() = panel
|
||||
|
||||
private fun doSelectFromDiskAction() {
|
||||
var jdkHome: String? = null
|
||||
SdkConfigurationUtil.selectSdkHome(sdkType) {
|
||||
jdkHome = it
|
||||
}
|
||||
|
||||
jdkHome?.let {
|
||||
resultingJDKHome = it
|
||||
close(OK_EXIT_CODE)
|
||||
}
|
||||
}
|
||||
|
||||
override fun doOKAction() {
|
||||
val installItem = selectedItem
|
||||
val installDir = File(FileUtil.expandUserHome(selectedPath))
|
||||
|
||||
val validateError = runCatching {
|
||||
JDKInstaller.validateInstallDir(installDir)
|
||||
}.exceptionOrNull()
|
||||
|
||||
if (validateError != null) {
|
||||
//TODO: review
|
||||
setErrorText(validateError.message)
|
||||
return
|
||||
}
|
||||
|
||||
ProgressManager.getInstance().run(object: Task.Modal(project, "Installing JDK...", true) {
|
||||
override fun run(indicator: ProgressIndicator) {
|
||||
val installError = runCatching {
|
||||
JDKInstaller.installJDK(installItem, installDir, indicator)
|
||||
}.exceptionOrNull()
|
||||
|
||||
if (installError == null) {
|
||||
return invokeLater {
|
||||
resultingJDKHome = installDir.absolutePath
|
||||
superDoOKAction()
|
||||
}
|
||||
}
|
||||
|
||||
setErrorText(installError.message)
|
||||
LOG.warn("Failed to install JDK $installItem to $installDir. ${installError.message}", installError)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun superDoOKAction() = super.doOKAction()
|
||||
|
||||
// returns unpacked JDK location (if any) or null if cancelled
|
||||
fun selectOrDownloadAndUnpackJDK(): String? = when {
|
||||
showAndGet() -> resultingJDKHome
|
||||
else -> null
|
||||
}
|
||||
|
||||
private inline fun TextFieldWithBrowseButton.onTextChange(crossinline action: (String) -> Unit) {
|
||||
textField.document.addDocumentListener(object : DocumentListener {
|
||||
override fun changedUpdate(e: DocumentEvent?) = action(text)
|
||||
override fun insertUpdate(e: DocumentEvent?) = action(text)
|
||||
override fun removeUpdate(e: DocumentEvent?) = action(text)
|
||||
})
|
||||
}
|
||||
|
||||
private inline fun <reified T> ComboBox<T>.onSelectionChange(crossinline action: (T) -> Unit) {
|
||||
this.addItemListener { e ->
|
||||
if (e.stateChange == ItemEvent.SELECTED) action(e.item as T)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object JDKInstaller {
|
||||
fun validateInstallDir(targetDir: File) {
|
||||
if (targetDir.isFile) throw RuntimeException("Failed to extract JDK. Target path is an existing file")
|
||||
if (targetDir.isDirectory && targetDir.listFiles()?.isNotEmpty() == true) {
|
||||
throw RuntimeException("Failed to extract JDK. Target path is an existing non-empty directory")
|
||||
}
|
||||
}
|
||||
|
||||
fun installJDK(item: JDKDownloadItem, targetDir: File, indicator: ProgressIndicator?) {
|
||||
indicator?.text = "Installing ${item.vendor.vendor} ${item.version}..."
|
||||
|
||||
validateInstallDir(targetDir)
|
||||
|
||||
val url = Urls.parse(item.url, false) ?: error("Cannot parse download URL: ${item.url}")
|
||||
if (!url.scheme.equals("https", ignoreCase = true)) error("URL must use https:// protocol, but was: $url")
|
||||
|
||||
indicator?.text2 = "Downloading $url"
|
||||
val downloadPath = File(PathManager.getTempPath(), "jdk-${item.installFolderName}")
|
||||
try {
|
||||
try {
|
||||
HttpRequests
|
||||
.request(item.url)
|
||||
.productNameAsUserAgent()
|
||||
.connect { processor -> processor.saveToFile(downloadPath, indicator) }
|
||||
|
||||
}
|
||||
catch (t: IOException) {
|
||||
throw RuntimeException("Failed to download JDK from $url. ${t.message}", t)
|
||||
}
|
||||
|
||||
val sizeDiff = downloadPath.length() - item.archiveSize
|
||||
if (sizeDiff != 0L) {
|
||||
throw RuntimeException("Downloaded JDK distribution has incorrect size, difference is ${sizeDiff.absoluteValue} bytes")
|
||||
}
|
||||
|
||||
val actualHashCode = Files.asByteSource(downloadPath).hash(Hashing.sha256()).toString()
|
||||
if (!actualHashCode.equals(item.sha256, ignoreCase = true)) {
|
||||
throw RuntimeException("SHA-256 checksum does not match. Actual value is $actualHashCode, expected ${item.sha256}")
|
||||
}
|
||||
|
||||
indicator?.isIndeterminate = true
|
||||
indicator?.text2 = "Unpacking"
|
||||
|
||||
val decompressor = when (item.archiveType) {
|
||||
"zip" -> Decompressor.Zip(downloadPath)
|
||||
"targz" -> Decompressor.Tar(downloadPath)
|
||||
else -> error("Unsupported archiveType: ${item.archiveSize}")
|
||||
}
|
||||
//handle cancellation via postProcessor (instead of inheritance)
|
||||
decompressor.postprocessor { indicator?.checkCanceled() }
|
||||
decompressor.cutDirs(item.archiveCutDirs)
|
||||
decompressor.extract(targetDir)
|
||||
} catch (t: Throwable) {
|
||||
//if we were cancelled in the middle or failed, let's clean up
|
||||
FileUtil.delete(targetDir)
|
||||
throw t
|
||||
} finally {
|
||||
FileUtil.delete(downloadPath)
|
||||
}
|
||||
get() = getInstanceIfEnabled() != null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
|
||||
package com.intellij.openapi.projectRoots.impl
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode
|
||||
import com.intellij.openapi.diagnostic.logger
|
||||
import com.intellij.openapi.progress.ProgressIndicator
|
||||
import com.intellij.openapi.util.SystemInfo
|
||||
import com.intellij.openapi.util.registry.Registry
|
||||
import com.intellij.util.io.HttpRequests
|
||||
import org.tukaani.xz.XZInputStream
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.IOException
|
||||
import java.lang.RuntimeException
|
||||
|
||||
data class JDKVendor(
|
||||
val vendor: String
|
||||
//TODO: add JDK type (e.g. Adopt OpenJDK / Adopt OpenJ9) (feed update required?)
|
||||
)
|
||||
|
||||
data class JDKDownloadItem(
|
||||
val vendor: JDKVendor,
|
||||
val version: String,
|
||||
//TODO: add order value for comparison (we'd like 13, 11, 9, 8) order
|
||||
//TODO: include vendor specific version too (e.g. Zulu 13.12.123)
|
||||
//TODO: add vendor specific flavour (OpenJ9, JavaFX)
|
||||
val arch: String,
|
||||
val archiveType: String, //TODO: rename in JSON
|
||||
val url: String,
|
||||
val size: Long,
|
||||
val sha256: String
|
||||
) {
|
||||
val unpackedSize: Long get() = size //TODO: implement it in the feed
|
||||
val archiveSize: Long get() = size //TODO: implement it in the feed
|
||||
|
||||
// archives normally have an empty directory inside, we need to get rid of it for some cases
|
||||
val archiveCutDirs: Int = 1 // TODO: implement in the feed
|
||||
|
||||
val installFileName get() = url.split("/").last() //TODO: use feed for it
|
||||
val installFolderName get() = installFileName.removeSuffix(".zip").removeSuffix(".tar.gz") //TODO: use feed for it
|
||||
}
|
||||
|
||||
object JDKListDownloader {
|
||||
private val LOG = logger<JDKListDownloader>()
|
||||
|
||||
private val feedUrl: String
|
||||
get() {
|
||||
val registry = runCatching { Registry.get("jdk.downloader.url").asString() }.getOrNull()
|
||||
if (!registry.isNullOrBlank()) return registry
|
||||
|
||||
//TODO: let's use CDN URL in once it'd be established
|
||||
return "https://buildserver.labs.intellij.net/guestAuth/repository/download/ijplatform_master_Service_GenerateJDKsJson/lasest.lastSuccessful/feed.zip!/jdks.json.xz"
|
||||
}
|
||||
|
||||
fun downloadModel(progress: ProgressIndicator?, feedUrl : String = this.feedUrl): List<JDKDownloadItem> {
|
||||
//we download XZ packed version of the data (several KBs packed, several dozen KBs unpacked) and process it in-memory
|
||||
val rawData = try {
|
||||
//timeouts are handled inside
|
||||
HttpRequests
|
||||
.request(feedUrl)
|
||||
.productNameAsUserAgent()
|
||||
.readBytes(progress)
|
||||
.unXZ()
|
||||
} catch (t: IOException) {
|
||||
throw RuntimeException("Failed to download and process the JDKs list from $feedUrl. ${t.message}", t)
|
||||
}
|
||||
|
||||
try {
|
||||
val tree = ObjectMapper().readTree(rawData) as? ObjectNode ?: error("Unexpected JSON data")
|
||||
val items = tree["jdks"] as? ArrayNode ?: error("`jdks` element is missing")
|
||||
|
||||
val expectedOS = when {
|
||||
SystemInfo.isWindows -> "windows"
|
||||
SystemInfo.isMac -> "mac"
|
||||
SystemInfo.isLinux -> "linux"
|
||||
else -> error("Unsupported OS")
|
||||
}
|
||||
|
||||
val result = mutableListOf<JDKDownloadItem>()
|
||||
for (item in items.filterIsInstance<ObjectNode>()) {
|
||||
val vendor = item["vendor"]?.asText() ?: continue
|
||||
val version = item["jdk_version"]?.asText() ?: continue
|
||||
val packages = item["packages"] as? ArrayNode ?: continue
|
||||
val pkg = packages.filterIsInstance<ObjectNode>().singleOrNull { it["os"]?.asText() == expectedOS } ?: continue
|
||||
val arch = pkg["arch"]?.asText() ?: continue
|
||||
val fileType = pkg["package"]?.asText() ?: continue
|
||||
val url = pkg["url"]?.asText() ?: continue
|
||||
val size = pkg["size"]?.asLong() ?: continue
|
||||
val sha256 = pkg["sha256"]?.asText() ?: continue
|
||||
|
||||
result += JDKDownloadItem(vendor = JDKVendor(vendor),
|
||||
version = version,
|
||||
arch = arch,
|
||||
archiveType = fileType,
|
||||
url = url,
|
||||
size = size,
|
||||
sha256 = sha256)
|
||||
}
|
||||
|
||||
return result
|
||||
} catch (t: Throwable) {
|
||||
throw RuntimeException("Failed to parse downloaded JDKs list from $feedUrl. ${t.message}", t)
|
||||
}
|
||||
}
|
||||
|
||||
private fun ByteArray.unXZ() = ByteArrayInputStream(this).use { input ->
|
||||
XZInputStream(input).use { it.readBytes() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -599,8 +599,8 @@ public final class JavaSdkImpl extends JavaSdk {
|
||||
@NotNull JComponent parentComponent,
|
||||
@Nullable Sdk selectedSdk,
|
||||
@NotNull Consumer<Sdk> sdkCreatedCallback) {
|
||||
JDKDownloaderService instance = JDKDownloaderService.getInstance();
|
||||
JDKDownloaderService instance = JDKDownloaderService.getInstanceIfEnabled();
|
||||
if (instance == null) return;
|
||||
instance.showCustomCreateUI(this, sdkModel, parentComponent, sdkCreatedCallback);
|
||||
instance.downloadOrSelectJDK(this, sdkModel, parentComponent, sdkCreatedCallback);
|
||||
}
|
||||
}
|
||||
+115
-17
@@ -1,7 +1,12 @@
|
||||
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
|
||||
package com.intellij.openapi.projectRoots.impl
|
||||
|
||||
import com.intellij.jdkDownloader.JDKItem
|
||||
import com.intellij.jdkDownloader.JDKInstaller
|
||||
import com.intellij.jdkDownloader.JDKListDownloader
|
||||
import com.intellij.jdkDownloader.JDKProduct
|
||||
import com.intellij.testFramework.rules.TempDirectory
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.Assert
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
@@ -17,31 +22,124 @@ class JDKDownloaderTest {
|
||||
Assert.assertTrue(data.isNotEmpty())
|
||||
}
|
||||
|
||||
val vendor = JDKVendor(("mock"))
|
||||
val mockTarGZ = JDKDownloadItem(vendor = vendor, version = "1231", arch = "x", archiveType = "targz",
|
||||
url = "https://repo.labs.intellij.net/idea-test-data/jdk-download-test-data.tar.gz",
|
||||
size = 249,
|
||||
sha256 = "ffc8825d96e3f89cb4a8ca64b9684c37f55d6c5bd54628ebf984f8282f8a59ff")
|
||||
val mockZip = JDKDownloadItem(vendor = vendor, version = "1231", arch = "x", archiveType = "zip",
|
||||
url = "https://repo.labs.intellij.net/idea-test-data/jdk-download-test-data.zip",
|
||||
size = 604,
|
||||
sha256 = "1cf15536c1525f413190fd53243f343511a17e6ce7439ccee4dc86f0d34f9e81")
|
||||
private fun jdkItemForTest(url: String,
|
||||
packageType: String,
|
||||
size: Long,
|
||||
sha256: String,
|
||||
cutDirs: Int = 0) = JDKItem(
|
||||
JDKProduct("Vendor", null, null),
|
||||
false,
|
||||
123,
|
||||
"123.123",
|
||||
null,
|
||||
null,
|
||||
"jetbrains-hardware",
|
||||
packageType,
|
||||
url,
|
||||
sha256,
|
||||
size,
|
||||
10 * size,
|
||||
cutDirs,
|
||||
"",
|
||||
url.split("/").last(),
|
||||
url.split("/").last().removeSuffix(".tar.gz").removeSuffix(".zip")
|
||||
)
|
||||
|
||||
private val mockTarGZ = jdkItemForTest(packageType = "targz",
|
||||
url = "https://repo.labs.intellij.net/idea-test-data/jdk-download-test-data.tar.gz",
|
||||
size = 249,
|
||||
sha256 = "ffc8825d96e3f89cb4a8ca64b9684c37f55d6c5bd54628ebf984f8282f8a59ff"
|
||||
)
|
||||
private val mockZip = jdkItemForTest(packageType = "zip",
|
||||
url = "https://repo.labs.intellij.net/idea-test-data/jdk-download-test-data.zip",
|
||||
size = 604,
|
||||
sha256 = "1cf15536c1525f413190fd53243f343511a17e6ce7439ccee4dc86f0d34f9e81")
|
||||
|
||||
@Test
|
||||
fun `unpacking targz`() = testUnpacking(mockTarGZ) {dir ->
|
||||
Assert.assertTrue(File(dir, "TheApp/FooBar.app/theApp").isFile)
|
||||
Assert.assertTrue(File(dir, "TheApp/QPCV/ggg.txt").isFile)
|
||||
fun `unpacking targz`() = testUnpacking(mockTarGZ) { dir ->
|
||||
assertThat(File(dir, "TheApp/FooBar.app/theApp")).isFile()
|
||||
assertThat(File(dir, "TheApp/QPCV/ggg.txt")).isFile()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unpacking zip`() = testUnpacking(mockZip) {dir ->
|
||||
Assert.assertTrue(File(dir, "folder/readme2").isDirectory)
|
||||
Assert.assertTrue(File(dir, "folder/file").isFile)
|
||||
fun `unpacking targz cut dirs`() = testUnpacking(mockTarGZ.copy(unpackCutDirs = 2)) { dir ->
|
||||
assertThat(File(dir, "theApp")).isFile()
|
||||
assertThat(File(dir, "ggg.txt")).isFile()
|
||||
}
|
||||
|
||||
private fun testUnpacking(item: JDKDownloadItem, resultDir: (File) -> Unit) {
|
||||
@Test
|
||||
fun `unpacking targz cut dirs and prefix`() = testUnpacking(
|
||||
mockTarGZ.copy(
|
||||
unpackCutDirs = 2,
|
||||
unpackPrefixFilter = "TheApp/FooBar.app")
|
||||
) { dir ->
|
||||
assertThat(File(dir, "theApp")).isFile()
|
||||
assertThat(File(dir, "ggg.txt")).doesNotExist()
|
||||
}
|
||||
|
||||
@Test(expected = Exception::class)
|
||||
fun `unpacking targz invalid size`() = testUnpacking(mockTarGZ.copy(archiveSize = 234234)) { dir ->
|
||||
assertThat(File(dir, "TheApp/FooBar.app/theApp")).isFile()
|
||||
assertThat(File(dir, "TheApp/QPCV/ggg.txt")).isFile()
|
||||
}
|
||||
|
||||
@Test(expected = Exception::class)
|
||||
fun `unpacking targz invalid checksum`() = testUnpacking(mockTarGZ.copy(sha256 = "234234")) { dir ->
|
||||
assertThat(File(dir, "TheApp/FooBar.app/theApp")).isFile()
|
||||
assertThat(File(dir, "TheApp/QPCV/ggg.txt")).isFile()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unpacking zip`() = testUnpacking(mockZip) { dir ->
|
||||
assertThat(File(dir, "folder/readme2")).isDirectory()
|
||||
assertThat(File(dir, "folder/file")).isFile()
|
||||
}
|
||||
|
||||
@Test(expected = Exception::class)
|
||||
fun `unpacking zip invalid size`() = testUnpacking(mockZip.copy(archiveSize = 234)) { dir ->
|
||||
assertThat(File(dir, "folder/readme2")).isDirectory()
|
||||
assertThat(File(dir, "folder/file")).isFile()
|
||||
}
|
||||
|
||||
@Test(expected = Exception::class)
|
||||
fun `unpacking zip invalid checksum`() = testUnpacking(mockZip.copy(sha256 = "234")) { dir ->
|
||||
assertThat(File(dir, "folder/readme2")).isDirectory()
|
||||
assertThat(File(dir, "folder/file")).isFile()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unpacking zip cut dirs and wrong prefix`() = testUnpacking(
|
||||
mockZip.copy(
|
||||
unpackCutDirs = 1,
|
||||
unpackPrefixFilter = "wrong")
|
||||
) { dir ->
|
||||
assertThat(File(dir, "folder/readme2")).doesNotExist()
|
||||
assertThat(File(dir, "folder/file")).doesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unpacking zip cut dirs and prefix`() = testUnpacking(
|
||||
mockZip.copy(
|
||||
unpackCutDirs = 1,
|
||||
unpackPrefixFilter = "folder")
|
||||
) { dir ->
|
||||
assertThat(File(dir, "readme2")).isDirectory()
|
||||
assertThat(File(dir, "file")).isFile()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unpacking zip and prefix`() = testUnpacking(
|
||||
mockZip.copy(
|
||||
unpackCutDirs = 0,
|
||||
unpackPrefixFilter = "folder/file")
|
||||
) { dir ->
|
||||
assertThat(File(dir, "readme2")).doesNotExist()
|
||||
assertThat(File(dir, "folder/file")).isFile()
|
||||
}
|
||||
|
||||
private inline fun testUnpacking(item: JDKItem, resultDir: (File) -> Unit) {
|
||||
val dir = fsRule.newFolder()
|
||||
JDKInstaller.installJDK(item, dir, null)
|
||||
JDKInstaller.installJDK(item, dir.absolutePath, null)
|
||||
resultDir(dir)
|
||||
}
|
||||
}
|
||||
|
||||
+10
-1
@@ -43,7 +43,9 @@ import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.awt.*;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
@@ -308,6 +310,13 @@ public class SdkConfigurationUtil {
|
||||
}
|
||||
|
||||
public static void selectSdkHome(@NotNull final SdkType sdkType, @NotNull final Consumer<? super String> consumer) {
|
||||
selectSdkHome(sdkType, null, null, consumer);
|
||||
}
|
||||
|
||||
public static void selectSdkHome(@NotNull final SdkType sdkType,
|
||||
@Nullable Project project,
|
||||
@Nullable Component component,
|
||||
@NotNull final Consumer<? super String> consumer) {
|
||||
final FileChooserDescriptor descriptor = sdkType.getHomeChooserDescriptor();
|
||||
if (ApplicationManager.getApplication().isUnitTestMode()) {
|
||||
Sdk sdk = ProjectJdkTable.getInstance().findMostRecentSdkOfType(sdkType);
|
||||
@@ -315,7 +324,7 @@ public class SdkConfigurationUtil {
|
||||
consumer.consume(sdk.getHomePath());
|
||||
return;
|
||||
}
|
||||
FileChooser.chooseFiles(descriptor, null, getSuggestedSdkRoot(sdkType), chosen -> {
|
||||
FileChooser.chooseFiles(descriptor, project, component, getSuggestedSdkRoot(sdkType), chosen -> {
|
||||
final String path = chosen.get(0).getPath();
|
||||
if (sdkType.isValidSdkHome(path)) {
|
||||
consumer.consume(path);
|
||||
|
||||
+7
-1
@@ -268,10 +268,16 @@ public class ProjectSdksModel implements SdkModel {
|
||||
}
|
||||
|
||||
public void addSdk(@NotNull SdkType type, @NotNull String home, @Nullable Consumer<? super Sdk> callback) {
|
||||
final Sdk newJdk = createSdk(type, home);
|
||||
setupSdk(newJdk, callback);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Sdk createSdk(@NotNull SdkType type, @NotNull String home) {
|
||||
String newSdkName = SdkConfigurationUtil.createUniqueSdkName(type, home, myProjectSdks.values());
|
||||
final ProjectJdkImpl newJdk = new ProjectJdkImpl(newSdkName, type);
|
||||
newJdk.setHomePath(home);
|
||||
setupSdk(newJdk, callback);
|
||||
return newJdk;
|
||||
}
|
||||
|
||||
private void setupSdk(@NotNull Sdk newJdk, @Nullable Consumer<? super Sdk> callback) {
|
||||
|
||||
Reference in New Issue
Block a user