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:
Eugene Petrenko
2019-11-07 02:40:58 +00:00
committed by intellij-monorepo-bot
parent f581a02207
commit b1f5fce9ed
12 changed files with 678 additions and 410 deletions
+4
View File
@@ -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() }
}
}
-4
View File
@@ -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>
+2 -1
View File
@@ -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);
}
}
@@ -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)
}
}
@@ -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);
@@ -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) {