From 83d08231ed69317e2a5bba885e13ac96b98a850b Mon Sep 17 00:00:00 2001 From: Katsiaryna Tsytsenia Date: Wed, 17 Apr 2024 12:46:54 +0300 Subject: [PATCH] IJMP-1522 Added the ability to create a Zote Team Configuration file Zowe Team Configuration file can be created for the current project directly from the plugin New: src/main/kotlin/org/zowe/explorer/config/connect/ui/zosmf/ZoweTeamConfigDialog.kt src/main/kotlin/org/zowe/explorer/explorer/actions/AddZoweTeamConfigAction.kt src/main/resources/files/zowe.config.json src/main/resources/files/zowe.schema.json src/main/kotlin/org/zowe/explorer/config/connect/ui/zosmf/CommonConnectionDialog.kt src/test/kotlin/org/zowe/explorer/config/ZoweConfigTestSpec.kt Modified: build.gradle.kts src/main/kotlin/org/zowe/explorer/config/connect/ui/zosmf/ConnectionDialog.kt src/main/kotlin/org/zowe/explorer/config/connect/ui/zosmf/ZOSMFConnectionConfigurable.kt src/main/kotlin/org/zowe/explorer/zowe/ZoweStartupActivity.kt src/main/kotlin/org/zowe/explorer/zowe/service/ZoweConfigService.kt src/main/kotlin/org/zowe/explorer/zowe/service/ZoweConfigServiceImpl.kt src/main/kotlin/org/zowe/explorer/zowe/service/ZoweFileListener.kt src/main/resources/META-INF/plugin.xml Signed-off-by: Katsiaryna Tsytsenia --- build.gradle.kts | 2 +- .../ui/zosmf/CommonConnectionDialog.kt | 246 ++++++++++++ .../connect/ui/zosmf/ConnectionDialog.kt | 205 ++-------- .../ui/zosmf/ZOSMFConnectionConfigurable.kt | 77 ++-- .../connect/ui/zosmf/ZoweTeamConfigDialog.kt | 167 +++++++++ .../actions/AddZoweTeamConfigAction.kt | 88 +++++ .../zowe/explorer/zowe/ZoweStartupActivity.kt | 35 +- .../zowe/service/ZoweConfigService.kt | 25 +- .../zowe/service/ZoweConfigServiceImpl.kt | 184 +++++++-- .../explorer/zowe/service/ZoweFileListener.kt | 21 +- src/main/resources/META-INF/plugin.xml | 7 + src/main/resources/files/zowe.config.json | 46 +++ src/main/resources/files/zowe.schema.json | 350 ++++++++++++++++++ .../explorer/config/ZoweConfigTestSpec.kt | 202 ++++++++++ 14 files changed, 1404 insertions(+), 251 deletions(-) create mode 100644 src/main/kotlin/org/zowe/explorer/config/connect/ui/zosmf/CommonConnectionDialog.kt create mode 100644 src/main/kotlin/org/zowe/explorer/config/connect/ui/zosmf/ZoweTeamConfigDialog.kt create mode 100644 src/main/kotlin/org/zowe/explorer/explorer/actions/AddZoweTeamConfigAction.kt create mode 100644 src/main/resources/files/zowe.config.json create mode 100644 src/main/resources/files/zowe.schema.json create mode 100644 src/test/kotlin/org/zowe/explorer/config/ZoweConfigTestSpec.kt diff --git a/build.gradle.kts b/build.gradle.kts index 335a11ff8..bf73efc97 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -46,7 +46,7 @@ val junitVersion = "5.10.2" val mockkVersion = "1.13.10" val ibmMqVersion = "9.3.5.0" val jGraphTVersion = "1.5.2" -val zoweKotlinSdkVersion = "0.4.0" +val zoweKotlinSdkVersion = "0.5.0-rc.7" val javaKeytarVersion = "1.0.0" repositories { diff --git a/src/main/kotlin/org/zowe/explorer/config/connect/ui/zosmf/CommonConnectionDialog.kt b/src/main/kotlin/org/zowe/explorer/config/connect/ui/zosmf/CommonConnectionDialog.kt new file mode 100644 index 000000000..a1eaf6b08 --- /dev/null +++ b/src/main/kotlin/org/zowe/explorer/config/connect/ui/zosmf/CommonConnectionDialog.kt @@ -0,0 +1,246 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright IBA Group 2020 + */ + +package org.zowe.explorer.config.connect.ui.zosmf + +import com.intellij.icons.AllIcons +import com.intellij.openapi.components.service +import com.intellij.openapi.progress.ProcessCanceledException +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.MessageDialogBuilder +import com.intellij.openapi.ui.Messages +import com.intellij.ui.components.JBCheckBox +import com.intellij.ui.components.JBTextField +import org.zowe.explorer.common.message +import org.zowe.explorer.common.ui.DialogMode +import org.zowe.explorer.common.ui.StatefulDialog +import org.zowe.explorer.common.ui.showUntilDone +import org.zowe.explorer.config.connect.ConnectionConfig +import org.zowe.explorer.config.connect.CredentialService +import org.zowe.explorer.config.connect.whoAmI +import org.zowe.explorer.dataops.DataOpsManager +import org.zowe.explorer.dataops.operations.InfoOperation +import org.zowe.explorer.dataops.operations.ZOSInfoOperation +import org.zowe.explorer.utils.crudable.Crudable +import org.zowe.explorer.utils.runTask +import org.zowe.kotlinsdk.annotations.ZVersion +import java.awt.Component +import java.util.* +import javax.swing.JCheckBox +import javax.swing.JTextField + +abstract class CommonConnectionDialog( + protected val crudable: Crudable, + override var state: ConnectionDialogState = ConnectionDialogState(), + val project: Project? = null +) : StatefulDialog(project) { + + + protected lateinit var urlTextField: JTextField + + protected lateinit var sslCheckbox: JCheckBox + + /** + * Check if only the connection name has changed in the connection dialog + */ + private fun isOnlyConnectionNameChanged( + initialState: ConnectionDialogState, + state: ConnectionDialogState + ): Boolean { + return initialState.connectionName != state.connectionName && + initialState.connectionUrl == state.connectionUrl && + initialState.username == state.username && + initialState.password == state.password && + initialState.isAllowSsl == state.isAllowSsl + } + + /** + * Companion function which takes the instance of ConnectionDialog and checks its current state after OK button is pressed. + * @param dialog + * @return returns true if there are violations found, if no violations were found then returns false as a result of validation + */ + private fun validateSecureConnectionUsage(dialog: CommonConnectionDialog): Boolean { + val urlToCheck = dialog.urlTextField.text.trim().startsWith("http://", true) + val sslEnabledCheck = dialog.sslCheckbox.isSelected + if (urlToCheck || sslEnabledCheck) { + return dialog.showSelfSignedUsageWarningDialog(dialog.urlTextField, dialog.sslCheckbox) + } + return false + } + + /** + * Function shows the warning dialog if any violations found and resolves them in case "Back to safety" was clicked + * @param components - dialog components which have to be resolved to valid values + * @return result of the pressed button + */ + fun showSelfSignedUsageWarningDialog(vararg components: Component): Boolean { + // default return backToSafety + val backToSafety = true + val choice = Messages.showDialog( + project, + "Creating an unsecure connection (HTTP instead of HTTP(s) and/or using self-signed certificates) is not recommended.\n" + + "You do this at your own peril and risk, and we do not bear any responsibility for the possible consequences of using this type of connection.\n" + + "Please contact your system administrator to configure your system to be able to create a secure connection.\n\n" + + "Do you want to proceed anyway?", + "Attempt to create an unsecured connection", + arrayOf( + "Back to safety", + "Proceed" + ), + 0, + AllIcons.General.WarningDialog, + null + ) + return when (choice) { + 0 -> { + components.forEach { + if (it is JBCheckBox) it.isSelected = false + if (it is JBTextField) it.text = it.text.replace("http:", "https:", true) + } + backToSafety + } + + 1 -> !backToSafety + else -> backToSafety + } + } + + abstract fun createConnectionDialog( + crudable: Crudable, + state: ConnectionDialogState = ConnectionDialogState(), + project: Project? = null + ): CommonConnectionDialog + + /** Show Test connection dialog and test the connection regarding the dialog state. + * First the method checks whether connection succeeds for specified user/password. + * If connection succeeds then the method automatically fill in z/OS version for this connection. + * We do not need to worry about choosing z/OS version manually from combo box. + * */ + fun showAndTestConnectionCommon( + crudable: Crudable, + parentComponent: Component? = null, + project: Project? = null, + initialState: ConnectionDialogState + ): ConnectionDialogState? { + var connectionDialog = this + val initState = initialState.clone() + return showUntilDone( + initialState = initialState, + factory = { connectionDialog }, + test = { state -> + + if (validateSecureConnectionUsage(connectionDialog)) { + state.connectionUrl = connectionDialog.urlTextField.text + state.isAllowSsl = connectionDialog.sslCheckbox.isSelected + connectionDialog = createConnectionDialog( + crudable, + initialState, + project + ) + return@showUntilDone false + } + + val newTestedConnConfig: ConnectionConfig + if (initialState.mode == DialogMode.UPDATE) { + if (isOnlyConnectionNameChanged(initState, state)) { + return@showUntilDone true + } + val newState = state.clone() + newState.initEmptyUuids(crudable) + newTestedConnConfig = ConnectionConfig( + newState.connectionUuid, + newState.connectionName, + newState.connectionUrl, + newState.isAllowSsl, + newState.zVersion + ) + CredentialService.instance.setCredentials( + connectionConfigUuid = newState.connectionUuid, + username = newState.username, + password = newState.password + ) + } else { + state.initEmptyUuids(crudable) + newTestedConnConfig = state.connectionConfig + CredentialService.instance.setCredentials( + connectionConfigUuid = state.connectionUuid, + username = state.username, + password = state.password + ) + } + val throwable = runTask(title = "Testing Connection to ${newTestedConnConfig.url}", project = project) { + return@runTask try { + runCatching { + service().performOperation(InfoOperation(newTestedConnConfig), it) + }.onSuccess { + state.owner = whoAmI(newTestedConnConfig) ?: "" + val systemInfo = + service().performOperation(ZOSInfoOperation(newTestedConnConfig)) + state.zVersion = when (systemInfo.zosVersion) { + "04.25.00" -> ZVersion.ZOS_2_2 + "04.26.00" -> ZVersion.ZOS_2_3 + "04.27.00" -> ZVersion.ZOS_2_4 + "04.28.00" -> ZVersion.ZOS_2_5 + else -> ZVersion.ZOS_2_1 + } + }.onFailure { + throw it + } + null + } catch (t: Throwable) { + t + } + } + if (throwable != null) { + state.mode = DialogMode.UPDATE + val confirmMessage = "Do you want to add it anyway?" + val tMessage = if (throwable is ProcessCanceledException) { + message("explorer.cancel.by.user.error") + } else { + throwable.message?.let { + if (it.contains("Exception")) { + it.substring(it.lastIndexOf(":") + 2) + .replaceFirstChar { c -> if (c.isLowerCase()) c.titlecase(Locale.getDefault()) else c.toString() } + } else { + it + } + } + } + val message = if (tMessage != null) { + "$tMessage\n\n$confirmMessage" + } else { + confirmMessage + } + val addAnyway = MessageDialogBuilder + .yesNo( + title = "Error Creating Connection", + message = message + ).icon(AllIcons.General.ErrorDialog) + .run { + if (parentComponent != null) { + ask(parentComponent) + } else { + ask(project) + } + } + connectionDialog = createConnectionDialog( + crudable, + initialState, + project + ) + addAnyway + } else { + true + } + } + ) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/org/zowe/explorer/config/connect/ui/zosmf/ConnectionDialog.kt b/src/main/kotlin/org/zowe/explorer/config/connect/ui/zosmf/ConnectionDialog.kt index e4ecb7f1d..d67eca353 100644 --- a/src/main/kotlin/org/zowe/explorer/config/connect/ui/zosmf/ConnectionDialog.kt +++ b/src/main/kotlin/org/zowe/explorer/config/connect/ui/zosmf/ConnectionDialog.kt @@ -12,21 +12,15 @@ package org.zowe.explorer.config.connect.ui.zosmf import com.intellij.icons.AllIcons import com.intellij.openapi.components.service -import com.intellij.openapi.progress.ProcessCanceledException import com.intellij.openapi.progress.runBackgroundableTask import com.intellij.openapi.project.Project import com.intellij.openapi.ui.MessageDialogBuilder import com.intellij.openapi.ui.MessageType -import com.intellij.openapi.ui.Messages import com.intellij.openapi.ui.popup.Balloon import com.intellij.openapi.ui.popup.JBPopupFactory import com.intellij.ui.awt.RelativePoint -import com.intellij.ui.components.JBCheckBox -import com.intellij.ui.components.JBTextField import com.intellij.ui.dsl.builder.* -import org.zowe.explorer.common.message import org.zowe.explorer.common.ui.DialogMode -import org.zowe.explorer.common.ui.StatefulDialog import org.zowe.explorer.common.ui.showUntilDone import org.zowe.explorer.config.configCrudable import org.zowe.explorer.config.connect.* @@ -47,52 +41,23 @@ import javax.swing.* /** Dialog to add a new connection */ class ConnectionDialog( - private val crudable: Crudable, - override var state: ConnectionDialogState = ConnectionDialogState(), - val project: Project? = null -) : StatefulDialog(project) { + crudable: Crudable, + state: ConnectionDialogState = ConnectionDialogState(), + project: Project? = null +) : CommonConnectionDialog(crudable, state, project) { /** * Private field * In case of DialogMode.UPDATE takes the last successful state from crudable, takes default state otherwise */ private val lastSuccessfulState: ConnectionDialogState = - if(state.mode == DialogMode.UPDATE) crudable.find { it.uuid == state.connectionUuid } + if (state.mode == DialogMode.UPDATE) crudable.find { it.uuid == state.connectionUuid } .findAny() .orElseGet { state.connectionConfig } .toDialogState(crudable) else ConnectionDialogState() - companion object { - - /** - * Check if only the connection name has changed in the connection dialog - */ - private fun isOnlyConnectionNameChanged(initialState: ConnectionDialogState, state: ConnectionDialogState): Boolean { - return initialState.connectionName != state.connectionName && - initialState.connectionUrl == state.connectionUrl && - initialState.username == state.username && - initialState.password == state.password && - initialState.isAllowSsl == state.isAllowSsl - } - - /** - * Companion function which takes the instance of ConnectionDialog and checks its current state after OK button is pressed. - * @param dialog - * @return returns true if there are violations found, if no violations were found then returns false as a result of validation - */ - private fun validateSecureConnectionUsage(dialog : ConnectionDialog) : Boolean { - val urlToCheck = dialog.urlTextField.text.trim().startsWith("http://", true) - val sslEnabledCheck = dialog.sslCheckbox.isSelected - if (urlToCheck || sslEnabledCheck) { - return dialog.showSelfSignedUsageWarningDialog(dialog.urlTextField, dialog.sslCheckbox) - } - return false - } - /** Show Test connection dialog and test the connection regarding the dialog state. - * First the method checks whether connection succeeds for specified user/password. - * If connection succeeds then the method automatically fill in z/OS version for this connection. - * We do not need to worry about choosing z/OS version manually from combo box. - * */ + companion object { + //Call showAndTestConnectionCommon for current class @JvmStatic fun showAndTestConnection( crudable: Crudable, @@ -100,115 +65,26 @@ class ConnectionDialog( project: Project? = null, initialState: ConnectionDialogState ): ConnectionDialogState? { - var connectionDialog = ConnectionDialog(crudable, initialState, project) - val initState = initialState.clone() - return showUntilDone( - initialState = initialState, - factory = { connectionDialog }, - test = { state -> - - if (validateSecureConnectionUsage(connectionDialog)) { - state.connectionUrl = connectionDialog.urlTextField.text - state.isAllowSsl = connectionDialog.sslCheckbox.isSelected - connectionDialog = ConnectionDialog(crudable, state, project) - return@showUntilDone false - } - - val newTestedConnConfig : ConnectionConfig - if (initialState.mode == DialogMode.UPDATE) { - if (isOnlyConnectionNameChanged(initState, state)) { - return@showUntilDone true - } - val newState = state.clone() - newState.initEmptyUuids(crudable) - newTestedConnConfig = ConnectionConfig(newState.connectionUuid, newState.connectionName, newState.connectionUrl, newState.isAllowSsl, newState.zVersion) - CredentialService.instance.setCredentials( - connectionConfigUuid = newState.connectionUuid, - username = newState.username, - password = newState.password - ) - } else { - state.initEmptyUuids(crudable) - newTestedConnConfig = state.connectionConfig - CredentialService.instance.setCredentials( - connectionConfigUuid = state.connectionUuid, - username = state.username, - password = state.password) - } - val throwable = runTask(title = "Testing Connection to ${newTestedConnConfig.url}", project = project) { - return@runTask try { - runCatching { - service().performOperation(InfoOperation(newTestedConnConfig), it) - }.onSuccess { - state.owner = whoAmI(newTestedConnConfig) ?: "" - val systemInfo = service().performOperation(ZOSInfoOperation(newTestedConnConfig)) - state.zVersion = when (systemInfo.zosVersion) { - "04.25.00" -> ZVersion.ZOS_2_2 - "04.26.00" -> ZVersion.ZOS_2_3 - "04.27.00" -> ZVersion.ZOS_2_4 - "04.28.00" -> ZVersion.ZOS_2_5 - else -> ZVersion.ZOS_2_1 - } - }.onFailure { - throw it - } - null - } catch (t: Throwable) { - t - } - } - if (throwable != null) { - state.mode = DialogMode.UPDATE - val confirmMessage = "Do you want to add it anyway?" - val tMessage = if (throwable is ProcessCanceledException) { - message("explorer.cancel.by.user.error") - } else { - throwable.message?.let { - if (it.contains("Exception")) { - it.substring(it.lastIndexOf(":") + 2) - .replaceFirstChar { c -> if (c.isLowerCase()) c.titlecase(Locale.getDefault()) else c.toString() } - } else { - it - } - } - } - val message = if (tMessage != null) { - "$tMessage\n\n$confirmMessage" - } else { - confirmMessage - } - val addAnyway = MessageDialogBuilder - .yesNo( - title = "Error Creating Connection", - message = message - ).icon(AllIcons.General.ErrorDialog) - .run { - if (parentComponent != null) { - ask(parentComponent) - } else { - ask(project) - } - } - connectionDialog = ConnectionDialog(crudable, state, project) - addAnyway - } else { - true - } - } - ) + val connectionDialog = + ConnectionDialog(crudable, initialState, project) + return connectionDialog.showAndTestConnectionCommon(crudable, parentComponent, project, initialState) } } private val initialState = state.clone() - private lateinit var urlTextField: JTextField - - private lateinit var sslCheckbox: JCheckBox - init { isResizable = false } + override fun createConnectionDialog( + crudable: Crudable, + state: ConnectionDialogState, + project: Project? + ): CommonConnectionDialog { + return ConnectionDialog(crudable, state, project) + } + /** Create dialog with the fields */ override fun createCenterPanel(): JComponent { val sameWidthLabelsGroup = "CONNECTION_DIALOG_LABELS_WIDTH_GROUP" @@ -254,11 +130,11 @@ class ConnectionDialog( label("Username") .widthGroup(sameWidthLabelsGroup) ( - if (state.zoweConfigPath == null) - textField() - else - cell(JPasswordField()) - ) + if (state.zoweConfigPath == null) + textField() + else + cell(JPasswordField()) + ) .bindText(state::username) .validationOnApply { validateForBlank(it.text.trim(), it) @@ -427,40 +303,5 @@ class ConnectionDialog( } } - /** - * Function shows the warning dialog if any violations found and resolves them in case "Back to safety" was clicked - * @param components - dialog components which have to be resolved to valid values - * @return result of the pressed button - */ - private fun showSelfSignedUsageWarningDialog(vararg components : Component) : Boolean { - // default return backToSafety - val backToSafety = true - val choice = Messages.showDialog( - project, - "Creating an unsecure connection (HTTP instead of HTTP(s) and/or using self-signed certificates) is not recommended.\n" + - "You do this at your own peril and risk, and we do not bear any responsibility for the possible consequences of using this type of connection.\n" + - "Please contact your system administrator to configure your system to be able to create a secure connection.\n\n" + - "Do you want to proceed anyway?", - "Attempt to create an unsecured connection", - arrayOf( - "Back to safety", - "Proceed" - ), - 0, - AllIcons.General.WarningDialog, - null - ) - return when (choice) { - 0 -> { - components.forEach { - if (it is JBCheckBox) it.isSelected = false - if (it is JBTextField) it.text = it.text.replace("http", "https", true) - } - backToSafety - } - 1 -> !backToSafety - else -> backToSafety - } - } } diff --git a/src/main/kotlin/org/zowe/explorer/config/connect/ui/zosmf/ZOSMFConnectionConfigurable.kt b/src/main/kotlin/org/zowe/explorer/config/connect/ui/zosmf/ZOSMFConnectionConfigurable.kt index 5925cab49..2a53214d0 100644 --- a/src/main/kotlin/org/zowe/explorer/config/connect/ui/zosmf/ZOSMFConnectionConfigurable.kt +++ b/src/main/kotlin/org/zowe/explorer/config/connect/ui/zosmf/ZOSMFConnectionConfigurable.kt @@ -23,13 +23,9 @@ import org.zowe.explorer.common.ui.DEFAULT_ROW_HEIGHT import org.zowe.explorer.common.ui.DialogMode import org.zowe.explorer.common.ui.ValidatingTableView import org.zowe.explorer.common.ui.tableWithToolbar -import org.zowe.explorer.config.SandboxListener -import org.zowe.explorer.config.applySandbox +import org.zowe.explorer.config.* import org.zowe.explorer.config.connect.ConnectionConfig import org.zowe.explorer.config.connect.Credentials -import org.zowe.explorer.config.isSandboxModified -import org.zowe.explorer.config.rollbackSandbox -import org.zowe.explorer.config.sandboxCrudable import org.zowe.explorer.config.ws.FilesWorkingSetConfig import org.zowe.explorer.config.ws.JesWorkingSetConfig import org.zowe.explorer.config.ws.WorkingSetConfig @@ -42,6 +38,7 @@ import org.zowe.kotlinsdk.zowe.config.parseConfigJson import java.awt.event.MouseAdapter import java.awt.event.MouseEvent import java.net.URI +import kotlin.collections.set /** Create and manage Connections tab in settings */ @Suppress("DialogTitleCapitalization") @@ -132,16 +129,51 @@ class ZOSMFConnectionConfigurable : BoundSearchableConfigurable("z/OSMF Connecti } } - /** Generates a connection removal warning message that is used for working sets */ - private fun generateRemoveWarningMessage(wsUsages: List, wsType: String): StringBuilder { - val warningMessageBuilder = - StringBuilder("The following $wsType working sets use selected connections:
") - wsUsages.forEach { wsConfig -> - warningMessageBuilder.append(wsConfig.name).append(", ") + companion object { + /** + * Generates a connection removal warning message that is used for Files/JES working set + * @param wsUsages - list of working sets for the current connection + * @param wsType - working set type + * @return StringBuilder with result string. + */ + fun generateRemoveWarningMessage(wsUsages: List, wsType: String): StringBuilder { + val warningMessageBuilder = + StringBuilder("The following $wsType working sets use selected connections:
") + wsUsages.forEach { wsConfig -> + warningMessageBuilder.append(wsConfig.name).append(", ") + } + warningMessageBuilder.setLength(warningMessageBuilder.length - 2) + warningMessageBuilder.append(".
") + return warningMessageBuilder + } + + /** + * Creates a message dialog when a connection is deleted if it is used in any working set + * @param filesWsUsages - list of files working sets + * @param jesWsUsages - list of JES working sets + * @return number of button pressed + */ + fun warningMessageForDeleteConfig( + filesWsUsages: List, + jesWsUsages: List + ): Int { + val warningMessageBuilder = StringBuilder() + if (filesWsUsages.isNotEmpty()) { + warningMessageBuilder.append(generateRemoveWarningMessage(filesWsUsages, "Files")) + } + if (jesWsUsages.isNotEmpty()) { + warningMessageBuilder.append(generateRemoveWarningMessage(jesWsUsages, "JES")) + } + warningMessageBuilder.append("
Do you really want to remove it?") + + return Messages.showOkCancelDialog( + warningMessageBuilder.toString(), + "Warning", + "Yes", + "Cancel", + Messages.getWarningIcon() + ) } - warningMessageBuilder.setLength(warningMessageBuilder.length - 2) - warningMessageBuilder.append(".
") - return warningMessageBuilder } /** Remove connections with the warning before they are deleted */ @@ -163,22 +195,7 @@ class ZOSMFConnectionConfigurable : BoundSearchableConfigurable("z/OSMF Connecti return } - val warningMessageBuilder = StringBuilder() - if (filesWsUsages.isNotEmpty()) { - warningMessageBuilder.append(generateRemoveWarningMessage(filesWsUsages, "Files")) - } - if (jesWsUsages.isNotEmpty()) { - warningMessageBuilder.append(generateRemoveWarningMessage(jesWsUsages, "JES")) - } - warningMessageBuilder.append("
Do you really want to remove it?") - - val ret = Messages.showOkCancelDialog( - warningMessageBuilder.toString(), - "Warning", - "Yes", - "Cancel", - Messages.getWarningIcon() - ) + val ret = warningMessageForDeleteConfig(filesWsUsages, jesWsUsages) if (ret == Messages.OK) removeSelectedConnections() diff --git a/src/main/kotlin/org/zowe/explorer/config/connect/ui/zosmf/ZoweTeamConfigDialog.kt b/src/main/kotlin/org/zowe/explorer/config/connect/ui/zosmf/ZoweTeamConfigDialog.kt new file mode 100644 index 000000000..38bda0666 --- /dev/null +++ b/src/main/kotlin/org/zowe/explorer/config/connect/ui/zosmf/ZoweTeamConfigDialog.kt @@ -0,0 +1,167 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright IBA Group 2020 + */ + +package org.zowe.explorer.config.connect.ui.zosmf + +import com.intellij.openapi.project.Project +import com.intellij.ui.dsl.builder.* +import org.zowe.explorer.common.ui.DialogMode +import org.zowe.explorer.config.connect.ConnectionConfig +import org.zowe.explorer.utils.crudable.Crudable +import org.zowe.explorer.utils.crudable.find +import org.zowe.explorer.utils.removeTrailingSlashes +import org.zowe.explorer.utils.validateConnectionName +import org.zowe.explorer.utils.validateForBlank +import org.zowe.explorer.utils.validateZosmfUrl +import org.zowe.explorer.zowe.ZOWE_CONFIG_NAME +import java.awt.Component +import javax.swing.JCheckBox +import javax.swing.JComponent +import javax.swing.JPasswordField + +/** Dialog to add a new zowe config file */ +class ZoweTeamConfigDialog( + crudable: Crudable, + state: ConnectionDialogState = ConnectionDialogState(), + project: Project? = null +) : CommonConnectionDialog(crudable, state, project) { + + /** + * Private field + * In case of DialogMode.UPDATE takes the last successful state from crudable, takes default state otherwise + */ + private val lastSuccessfulState: ConnectionDialogState = + if (state.mode == DialogMode.UPDATE) crudable.find { it.uuid == state.connectionUuid } + .findAny() + .orElseGet { state.connectionConfig } + .toDialogState(crudable) else ConnectionDialogState() + + companion object { + //Call showAndTestConnectionCommon for current class + @JvmStatic + fun showAndTestConnection( + crudable: Crudable, + parentComponent: Component? = null, + project: Project? = null, + initialState: ConnectionDialogState + ): ConnectionDialogState? { + val connectionDialog = + ZoweTeamConfigDialog(crudable, initialState, project) + return connectionDialog.showAndTestConnectionCommon(crudable, parentComponent, project, initialState) + } + } + + private val initialState = state.clone() + + + private lateinit var globalConfigCheckbox: JCheckBox + + init { + isResizable = false + } + + /** Create object to use in abstract class */ + override fun createConnectionDialog( + crudable: Crudable, + state: ConnectionDialogState, + project: Project? + ): CommonConnectionDialog { + return ZoweTeamConfigDialog(crudable, state, project) + } + + /** Create dialog with the fields */ + override fun createCenterPanel(): JComponent { + val sameWidthLabelsGroup = "CONNECTION_DIALOG_LABELS_WIDTH_GROUP" + + state.zoweConfigPath = "${project?.basePath}/${ZOWE_CONFIG_NAME}" + val connectionName = "zowe-".plus(project?.name) + return panel { + row { + label("Connection name") + .widthGroup(sameWidthLabelsGroup) + if (state.zoweConfigPath == null) { + textField() + .bindText(state::connectionName) + .enabled(false) + .text(connectionName) + .validationOnApply { + it.text = it.text.trim() + validateForBlank(it) ?: validateConnectionName( + it, + initialState.connectionName.ifBlank { null }, + crudable + ) + } + .focused() + .align(AlignX.FILL) + } else { + textField() + .bindText(state::connectionName) + .enabled(false) + .text(connectionName) + .applyToComponent { isEditable = false } + .align(AlignX.FILL) + } + } + row { + label("Connection URL: ") + .widthGroup(sameWidthLabelsGroup) + textField() + .bindText(state::connectionUrl) + .validationOnApply { + it.text = it.text.trim().removeTrailingSlashes() + validateForBlank(it) ?: validateZosmfUrl(it) + } + .also { urlTextField = it.component } + .align(AlignX.FILL) + } + row { + label("Username") + .widthGroup(sameWidthLabelsGroup) + ( + cell(JPasswordField()) + ) + .bindText(state::username) + .validationOnApply { + validateForBlank(String(it.password).trim(), it) + } + .onApply { + state.username = state.username.trim().uppercase() + } + .align(AlignX.FILL) + } + row { + label("Password: ") + .widthGroup(sameWidthLabelsGroup) + cell(JPasswordField()) + .bindText(state::password) + .validationOnApply { validateForBlank(it) } + .align(AlignX.FILL) + } + indent { + row { + checkBox("Accept self-signed SSL certificates") + .bindSelected(state::isAllowSsl) + .also { sslCheckbox = it.component } + } + row { + checkBox("Create global Zowe Team Configuration file") + .enabled(false) + } + } + } + .withMinimumWidth(500) + } + + init { + init() + title = "Add Zowe Team Configuration file" + } +} diff --git a/src/main/kotlin/org/zowe/explorer/explorer/actions/AddZoweTeamConfigAction.kt b/src/main/kotlin/org/zowe/explorer/explorer/actions/AddZoweTeamConfigAction.kt new file mode 100644 index 000000000..2719a5484 --- /dev/null +++ b/src/main/kotlin/org/zowe/explorer/explorer/actions/AddZoweTeamConfigAction.kt @@ -0,0 +1,88 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright IBA Group 2020 + */ + +package org.zowe.explorer.explorer.actions + +import com.intellij.openapi.actionSystem.ActionUpdateThread +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.application.runReadAction +import com.intellij.openapi.components.service +import com.intellij.openapi.vfs.VirtualFileManager +import org.zowe.explorer.config.configCrudable +import org.zowe.explorer.config.connect.CredentialService +import org.zowe.explorer.config.connect.ui.zosmf.ConnectionDialogState +import org.zowe.explorer.config.connect.ui.zosmf.ZoweTeamConfigDialog +import org.zowe.explorer.config.connect.ui.zosmf.initEmptyUuids +import org.zowe.explorer.zowe.ZOWE_CONFIG_NAME +import org.zowe.explorer.zowe.service.ZoweConfigService +import java.nio.file.Path + + +/** + * Action for adding zowe team config file through UI. + */ +class AddZoweTeamConfigAction : AnAction() { + + override fun getActionUpdateThread(): ActionUpdateThread { + return ActionUpdateThread.EDT + } + + /** + * Updates the presentation of the AddZoweTeamConfig action. + * @see com.intellij.openapi.actionSystem.AnAction.update + */ + override fun update(e: AnActionEvent) { + val project = e.project ?: let { + e.presentation.isEnabled = false + e.presentation.description = "Configuration file can only be created in the project" + return + } + val zoweConfigLocation = "${project.basePath}/$ZOWE_CONFIG_NAME" + + runReadAction { + VirtualFileManager.getInstance().findFileByNioPath(Path.of(zoweConfigLocation)) + }?.let { + e.presentation.isEnabled = false + e.presentation.description = "$ZOWE_CONFIG_NAME already exists in the project" + } ?: return + } + + /** + * Shows Create Zowe Team Config dialog + * @see com.intellij.openapi.actionSystem.AnAction.actionPerformed + */ + override fun actionPerformed(e: AnActionEvent) { + val state = ZoweTeamConfigDialog.showAndTestConnection( + crudable = configCrudable, + project = e.project, + initialState = ConnectionDialogState().initEmptyUuids(configCrudable) + ) + if (state != null) { + val connectionConfig = state.connectionConfig + val project = e.project ?: let { + e.presentation.isEnabled = false + e.presentation.description = "$ZOWE_CONFIG_NAME already exists in the project" + return + } + val zoweConfigService = project.service() + zoweConfigService.addZoweConfigFile(state) + + CredentialService.instance.setCredentials(connectionConfig.uuid, state.username, state.password) + configCrudable.add(connectionConfig) + } else { + return + } + } + + override fun isDumbAware(): Boolean { + return true + } +} diff --git a/src/main/kotlin/org/zowe/explorer/zowe/ZoweStartupActivity.kt b/src/main/kotlin/org/zowe/explorer/zowe/ZoweStartupActivity.kt index a6bed7f4a..0663ee44b 100644 --- a/src/main/kotlin/org/zowe/explorer/zowe/ZoweStartupActivity.kt +++ b/src/main/kotlin/org/zowe/explorer/zowe/ZoweStartupActivity.kt @@ -10,6 +10,7 @@ package org.zowe.explorer.zowe +import com.intellij.icons.AllIcons import com.intellij.notification.NotificationGroupManager import com.intellij.notification.NotificationType import com.intellij.openapi.actionSystem.AnActionEvent @@ -17,6 +18,7 @@ import com.intellij.openapi.components.service import com.intellij.openapi.project.DumbAwareAction import com.intellij.openapi.project.Project import com.intellij.openapi.startup.StartupActivity +import com.intellij.openapi.ui.Messages import org.zowe.explorer.config.connect.ConnectionConfig import org.zowe.explorer.explorer.EXPLORER_NOTIFICATION_GROUP_ID import org.zowe.explorer.utils.subscribe @@ -39,8 +41,7 @@ fun showNotificationForAddUpdateZoweConfigIfNeeded(project: Project) { if (zoweConfigState == ZoweConfigState.NEED_TO_ADD) { NotificationGroupManager.getInstance().getNotificationGroup(EXPLORER_NOTIFICATION_GROUP_ID) - .createNotification("Zowe config file detected", NotificationType.INFORMATION) - .apply { + .createNotification("Zowe config file detected", NotificationType.INFORMATION).apply { subscribe(ZOWE_CONFIG_CHANGED, object : ZoweConfigHandler { override fun onConfigSaved(config: ZoweConfig, connectionConfig: ConnectionConfig) { hideBalloon() @@ -56,6 +57,36 @@ fun showNotificationForAddUpdateZoweConfigIfNeeded(project: Project) { } } +/** + * Checks if zowe config has been deleted to be synchronized with crudable configs and show dialog for delete zowe config connection. + * @param project - project instance to check zoweConfig. + * @return Nothing. + */ +fun showDialogForDeleteZoweConfigIfNeeded(project: Project) { + val zoweConfigService = project.service() + val zoweConfigState = zoweConfigService.getZoweConfigState() + if(zoweConfigState != ZoweConfigState.NEED_TO_ADD || zoweConfigState != ZoweConfigState.NOT_EXISTS) { + val choice = Messages.showDialog( + project, + "Zowe config file has been deleted.\n" + + "Would you like to delete the corresponding connection?\n" + + "If you decide to leave the connection, it will be converted to a regular connection (username will be visible).", + "Deleting Zowe Config connection", + arrayOf( + "Delete Connection", "Keep Connection" + ), + 0, + AllIcons.General.QuestionDialog, + null + ) + if (choice == 0) { + zoweConfigService.deleteZoweConfig() + } + } + zoweConfigService.zoweConfig = null + zoweConfigService.checkAndRemoveOldZoweConnection() +} + /** * ZoweStartupActivity is needed to scan for the presence of a file at the project startup. * @author Valiantsin Krus diff --git a/src/main/kotlin/org/zowe/explorer/zowe/service/ZoweConfigService.kt b/src/main/kotlin/org/zowe/explorer/zowe/service/ZoweConfigService.kt index c87fd5863..dfbfc35e3 100644 --- a/src/main/kotlin/org/zowe/explorer/zowe/service/ZoweConfigService.kt +++ b/src/main/kotlin/org/zowe/explorer/zowe/service/ZoweConfigService.kt @@ -13,6 +13,7 @@ package org.zowe.explorer.zowe.service import com.intellij.openapi.project.Project import com.intellij.util.messages.Topic import org.zowe.explorer.config.connect.ConnectionConfig +import org.zowe.explorer.config.connect.ui.zosmf.ConnectionDialogState import org.zowe.kotlinsdk.zowe.config.ZoweConfig @@ -59,7 +60,7 @@ interface ZoweConfigService { * SYNCHRONIZED if zowe.config.json and connection config are presented and their data are the same. * NOT_EXISTS if zowe.config.json file is not presented in project. */ - fun getZoweConfigState (scanProject: Boolean = true): ZoweConfigState + fun getZoweConfigState(scanProject: Boolean = true): ZoweConfigState /** * Adds or updates connection config related to zoweConnection @@ -67,7 +68,27 @@ interface ZoweConfigService { * @param checkConnection - Verify zowe connection by sending info request if true. * @return - ConnectionConfig that was added or updated. */ - fun addOrUpdateZoweConfig (scanProject: Boolean = true, checkConnection: Boolean = true): ConnectionConfig? + fun addOrUpdateZoweConfig(scanProject: Boolean = true, checkConnection: Boolean = true): ConnectionConfig? + + /** + * Deletes connection config related to zoweConnection + * @return - Nothing. + */ + fun deleteZoweConfig() + + /** + * Creates zowe.schema.json for the currrent project and adds credentials to the secret store + * @param state - ConnectionDialogState for new zowe-connection + * @return - Nothing. + */ + fun addZoweConfigFile(state: ConnectionDialogState) + + /** + * Checks all connections and removes linl to Zowe config file if it exists + * renames old connection if it is needed + * @return - Nothing. + */ + fun checkAndRemoveOldZoweConnection() companion object { fun getInstance(project: Project): ZoweConfigService = project.getService(ZoweConfigService::class.java) diff --git a/src/main/kotlin/org/zowe/explorer/zowe/service/ZoweConfigServiceImpl.kt b/src/main/kotlin/org/zowe/explorer/zowe/service/ZoweConfigServiceImpl.kt index 0b858693e..b5e655820 100644 --- a/src/main/kotlin/org/zowe/explorer/zowe/service/ZoweConfigServiceImpl.kt +++ b/src/main/kotlin/org/zowe/explorer/zowe/service/ZoweConfigServiceImpl.kt @@ -15,31 +15,50 @@ import com.intellij.notification.NotificationGroupManager import com.intellij.notification.NotificationType import com.intellij.notification.Notifications import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.application.runWriteAction import com.intellij.openapi.components.service import com.intellij.openapi.project.DumbAwareAction import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.Messages import com.intellij.openapi.vfs.VirtualFileManager +import com.intellij.util.ReflectionUtil +import org.apache.commons.io.FileUtils import org.zowe.explorer.config.ConfigService import org.zowe.explorer.config.connect.* +import org.zowe.explorer.config.connect.ui.zosmf.ConnectionDialogState +import org.zowe.explorer.config.connect.ui.zosmf.ZOSMFConnectionConfigurable.Companion.warningMessageForDeleteConfig +import org.zowe.explorer.config.ws.FilesWorkingSetConfig +import org.zowe.explorer.config.ws.JesWorkingSetConfig import org.zowe.explorer.dataops.DataOpsManager import org.zowe.explorer.dataops.operations.InfoOperation import org.zowe.explorer.dataops.operations.ZOSInfoOperation import org.zowe.explorer.explorer.EXPLORER_NOTIFICATION_GROUP_ID import org.zowe.explorer.utils.crudable.find +import org.zowe.explorer.utils.crudable.getAll import org.zowe.explorer.utils.runReadActionInEdtAndWait import org.zowe.explorer.utils.runTask import org.zowe.explorer.utils.sendTopic +import org.zowe.explorer.utils.toMutableList import org.zowe.explorer.zowe.ZOWE_CONFIG_NAME import org.zowe.kotlinsdk.annotations.ZVersion import org.zowe.kotlinsdk.zowe.config.ZoweConfig import org.zowe.kotlinsdk.zowe.config.parseConfigJson -import java.nio.file.Path +import java.io.File +import java.net.URI +import java.net.URL +import java.nio.charset.Charset +import java.nio.charset.StandardCharsets +import java.nio.file.* import java.util.* +import java.util.regex.Matcher +import java.util.regex.Pattern import java.util.stream.Collectors +import kotlin.collections.set + const val ZOWE_CONFIG_NOTIFICATION_GROUP_ID = "org.zowe.explorerzowe.service.ZoweConfigNotificationGroupId" -val ZOWE_PROJECT_PREFIX = "zowe-" +const val ZOWE_PROJECT_PREFIX = "zowe-" /** * ZoweConfigService base implementation. @@ -50,6 +69,23 @@ val ZOWE_PROJECT_PREFIX = "zowe-" */ class ZoweConfigServiceImpl(override val myProject: Project) : ZoweConfigService { + companion object { + + private fun getResourceUrl(strPath: String): URL? { + return ReflectionUtil.getGrandCallerClass()?.classLoader?.getResource(strPath) + } + + private fun getResourceContent(strPath: String, cs: Charset): String { + val url = getResourceUrl(strPath) + val array = url.toString().split("!") + val fs: FileSystem = FileSystems.newFileSystem(URI.create(array[0]), HashMap()) + val content = String(Files.readAllBytes(fs.getPath(array[1])), cs) + fs.close() + return content + } + + } + private val configCrudable = ConfigService.instance.crudable override var zoweConfig: ZoweConfig? = null @@ -121,16 +157,16 @@ class ZoweConfigServiceImpl(override val myProject: Project) : ZoweConfigService */ private fun notifyUiOnConnectionFailure(title: String, content: String) { NotificationGroupManager.getInstance().getNotificationGroup(EXPLORER_NOTIFICATION_GROUP_ID) - .createNotification(title, content, NotificationType.ERROR) - .apply { - addAction(object : DumbAwareAction("Add Anyway") { - override fun actionPerformed(e: AnActionEvent) { - addOrUpdateZoweConfig(checkConnection = false) - hideBalloon() - } - }) - notify(myProject) - } + .createNotification(title, content, NotificationType.ERROR) + .apply { + addAction(object : DumbAwareAction("Add Anyway") { + override fun actionPerformed(e: AnActionEvent) { + addOrUpdateZoweConfig(checkConnection = false) + hideBalloon() + } + }) + notify(myProject) + } } /** @@ -207,25 +243,125 @@ class ZoweConfigServiceImpl(override val myProject: Project) : ZoweConfigService } } + /** + * @see ZoweConfigService.deleteZoweConfig + */ + override fun deleteZoweConfig() { + try { + val zoweConnection = findExistingConnection() ?: throw Exception("Cannot get Zowe config") + + val filesWorkingSets = configCrudable.getAll().toMutableList() + val filesWsUsages = filesWorkingSets.filter { filesWsConfig -> + filesWsConfig.connectionConfigUuid == zoweConnection.uuid + } + + val jesWorkingSet = configCrudable.getAll().toMutableList() + val jesWsUsages = jesWorkingSet.filter { jesWsConfig -> + jesWsConfig.connectionConfigUuid == zoweConnection.uuid + } + + if (filesWsUsages.isEmpty() && jesWsUsages.isEmpty()) { + CredentialService.instance.clearCredentials(zoweConnection.uuid) + configCrudable.delete(zoweConnection) + return + } + + val ret = warningMessageForDeleteConfig(filesWsUsages, jesWsUsages) + + if (ret == Messages.OK) { + CredentialService.instance.clearCredentials(zoweConnection.uuid) + configCrudable.delete(zoweConnection) + } + + } catch (e: Exception) { + notifyError(e) + } + } + + /** + * @see ZoweConfigService.addZoweConfigFile + */ + override fun addZoweConfigFile(state: ConnectionDialogState) { + checkAndRemoveOldZoweConnection() + + val schemaFileName = "${myProject.basePath}/zowe.schema.json" + val jsonFileName = "${myProject.basePath}/${ZOWE_CONFIG_NAME}" + val charset: Charset = StandardCharsets.UTF_8 + + val pathSourceSchema = getResourceUrl("files/zowe.schema.json") + + val f: File = File(schemaFileName) + if (!f.exists()) { + FileUtils.copyURLToFile(pathSourceSchema, File(schemaFileName)) + } + + val urlRegex = + "(https?:\\/\\/)(www\\.)?([-a-zA-Z0-9@:%._\\+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6})\b?([-a-zA-Z0-9()@:%_\\+.~#?&\\/\\/=]*)" + val pattern: Pattern = Pattern.compile(urlRegex) + val matcher: Matcher = pattern.matcher(state.connectionUrl) + + var host = "localhost" + var port = "443" + if (matcher.matches()) { + host = matcher.group(3) + port = matcher.group(4).substring(1) + } + + var content = getResourceContent("files/$ZOWE_CONFIG_NAME", charset) + content = content.replace("".toRegex(), port) + content = content.replace("".toRegex(), "\"$host\"") + content = content.replace("".toRegex(), (!state.isAllowSsl).toString()) + Files.write(Paths.get(jsonFileName), content.toByteArray(charset)) + + runWriteAction { + val configCredentialsMap = mutableMapOf() + configCredentialsMap["profiles.base.properties.user"] = state.username + configCredentialsMap["profiles.base.properties.password"] = state.password + ZoweConfig.saveNewSecureProperties(jsonFileName, configCredentialsMap) + } + } + + override fun checkAndRemoveOldZoweConnection() { + val allConnections = configCrudable.getAll().toList() + val allConnectionsNames: MutableList = allConnections.map { it.name }.toMutableList() + + allConnections.filter { it.zoweConfigPath != null }.forEach { + var index = 1 + var newName = it.name + while (allConnectionsNames.contains(newName)) { + newName = it.name.plus(index.toString()) + index++ + } + allConnectionsNames.add(newName) + it.name = newName + it.zoweConfigPath = null + configCrudable.update(it) + } + } + /** * Converts ZoweConfig to ConnectionConfig. * @param uuid - uuid returned connection. * @return converted ConnectionConfig. */ - fun ZoweConfig.toConnectionConfig(uuid: String, zVersion: ZVersion = ZVersion.ZOS_2_1, owner: String = ""): ConnectionConfig { + fun ZoweConfig.toConnectionConfig( + uuid: String, + zVersion: ZVersion = ZVersion.ZOS_2_1, + owner: String = "" + ): ConnectionConfig { val basePath = if (basePath.last() == '/') basePath.dropLast(1) else basePath val domain = if (port == null) host else "${host}:${port}" val zoweUrl = "${protocol}://${domain}${basePath}" val isAllowSelfSigned = !(rejectUnauthorized ?: false) return ConnectionConfig( - uuid, - zoweConnectionName, - zoweUrl, - isAllowSelfSigned, - zVersion, - "${myProject.basePath}/${ZOWE_CONFIG_NAME}", - owner + uuid, + zoweConnectionName, + zoweUrl, + isAllowSelfSigned, + zVersion, + "${myProject.basePath}/${ZOWE_CONFIG_NAME}", + owner ) } @@ -235,7 +371,7 @@ class ZoweConfigServiceImpl(override val myProject: Project) : ZoweConfigService * @return converted ConnectionConfig. */ fun ZoweConfig.toConnectionConfig(zVersion: ZVersion = ZVersion.ZOS_2_1): ConnectionConfig = - toConnectionConfig(getOrCreateUuid(), zVersion) + toConnectionConfig(getOrCreateUuid(), zVersion) /** @@ -252,15 +388,15 @@ class ZoweConfigServiceImpl(override val myProject: Project) : ZoweConfigService val zoweConfig = zoweConfig ?: return ZoweConfigState.NOT_EXISTS val existingConnection = findExistingConnection() ?: return ZoweConfigState.NEED_TO_ADD val newConnection = zoweConfig.toConnectionConfig( - existingConnection.uuid, existingConnection.zVersion, existingConnection.owner + existingConnection.uuid, existingConnection.zVersion, existingConnection.owner ) val zoweUsername = zoweConfig.user ?: return ZoweConfigState.ERROR val zowePassword = zoweConfig.password ?: return ZoweConfigState.ERROR return if (existingConnection == newConnection && - getUsername(newConnection) == zoweUsername && - getPassword(newConnection) == zowePassword + getUsername(newConnection) == zoweUsername && + getPassword(newConnection) == zowePassword ) { ZoweConfigState.SYNCHRONIZED } else { diff --git a/src/main/kotlin/org/zowe/explorer/zowe/service/ZoweFileListener.kt b/src/main/kotlin/org/zowe/explorer/zowe/service/ZoweFileListener.kt index 74b54c8e2..92513d765 100644 --- a/src/main/kotlin/org/zowe/explorer/zowe/service/ZoweFileListener.kt +++ b/src/main/kotlin/org/zowe/explorer/zowe/service/ZoweFileListener.kt @@ -10,12 +10,14 @@ package org.zowe.explorer.zowe.service +import com.intellij.openapi.application.invokeLater import com.intellij.openapi.project.ProjectLocator import com.intellij.openapi.vfs.newvfs.BulkFileListener import com.intellij.openapi.vfs.newvfs.events.VFileDeleteEvent import com.intellij.openapi.vfs.newvfs.events.VFileEvent import org.zowe.explorer.utils.runIfTrue -import org.zowe.explorer.utils.service +import org.zowe.explorer.zowe.ZOWE_CONFIG_NAME +import org.zowe.explorer.zowe.showDialogForDeleteZoweConfigIfNeeded import org.zowe.explorer.zowe.showNotificationForAddUpdateZoweConfigIfNeeded /** @@ -25,7 +27,7 @@ import org.zowe.explorer.zowe.showNotificationForAddUpdateZoweConfigIfNeeded * @version 0.5 * @since 2021-02-12 */ -class ZoweFileListener: BulkFileListener { +class ZoweFileListener : BulkFileListener { /** * Updates zowe config by file events. @@ -33,24 +35,23 @@ class ZoweFileListener: BulkFileListener { * @param isBefore - true if event triggered before changes action and false otherwise. * @return Nothing. */ - private fun updateZoweConfig(events: MutableList, isBefore: Boolean) { + private fun updateZoweConfig(events: MutableList) { events.forEach { e -> val file = e.file ?: return - runIfTrue(file.name == "zowe.config.json") { + runIfTrue(file.name == ZOWE_CONFIG_NAME) { val projectForFile = ProjectLocator.getInstance().guessProjectForFile(file) ?: return if (e is VFileDeleteEvent) { - projectForFile.service().zoweConfig = null - } else if(!isBefore) { + invokeLater { + showDialogForDeleteZoweConfigIfNeeded(projectForFile) + } + } else { showNotificationForAddUpdateZoweConfigIfNeeded(projectForFile) } } } } - override fun before(events: MutableList) { - updateZoweConfig(events, true) - } override fun after(events: MutableList) { - updateZoweConfig(events, false) + updateZoweConfig(events) } } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 0b467f8d9..e4f217c57 100755 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -429,6 +429,12 @@ Thank you for considering IBA Group for your mainframe needs. text="Connection" icon="AllIcons.Javaee.WebService"/> + + @@ -741,6 +747,7 @@ Thank you for considering IBA Group for your mainframe needs. icon="AllIcons.General.Add" popup="true"> + diff --git a/src/main/resources/files/zowe.config.json b/src/main/resources/files/zowe.config.json new file mode 100644 index 000000000..af606345a --- /dev/null +++ b/src/main/resources/files/zowe.config.json @@ -0,0 +1,46 @@ +{ + "$schema": "./zowe.schema.json", + "profiles": { + "zosmf": { + "type": "zosmf", + "properties": { + "port": + }, + "secure": [] + }, + "tso": { + "type": "tso", + "properties": { + "account": "", + "codePage": "1047", + "logonProcedure": "IZUFPROC" + }, + "secure": [] + }, + "ssh": { + "type": "ssh", + "properties": { + "port": 22 + }, + "secure": [] + }, + "base": { + "type": "base", + "properties": { + "host": , + "rejectUnauthorized": + }, + "secure": [ + "user", + "password" + ] + } + }, + "defaults": { + "zosmf": "zosmf", + "tso": "tso", + "ssh": "ssh", + "base": "base" + }, + "autoStore": true +} \ No newline at end of file diff --git a/src/main/resources/files/zowe.schema.json b/src/main/resources/files/zowe.schema.json new file mode 100644 index 000000000..36eb766c9 --- /dev/null +++ b/src/main/resources/files/zowe.schema.json @@ -0,0 +1,350 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$version": "1.0", + "type": "object", + "description": "Zowe configuration", + "properties": { + "profiles": { + "type": "object", + "description": "Mapping of profile names to profile configurations", + "patternProperties": { + "^\\S*$": { + "type": "object", + "description": "Profile configuration object", + "properties": { + "type": { + "description": "Profile type", + "type": "string", + "enum": [ + "zosmf", + "tso", + "ssh", + "base" + ] + }, + "properties": { + "description": "Profile properties object", + "type": "object" + }, + "profiles": { + "description": "Optional subprofile configurations", + "type": "object", + "$ref": "#/properties/profiles" + }, + "secure": { + "description": "Secure property names", + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } + }, + "allOf": [ + { + "if": { + "properties": { + "type": false + } + }, + "then": { + "properties": { + "properties": { + "title": "Missing profile type" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "zosmf" + } + } + }, + "then": { + "properties": { + "properties": { + "type": "object", + "title": "z/OSMF Profile", + "description": "z/OSMF Profile", + "properties": { + "host": { + "type": "string", + "description": "The z/OSMF server host name." + }, + "port": { + "type": "number", + "description": "The z/OSMF server port.", + "default": 443 + }, + "user": { + "type": "string", + "description": "Mainframe (z/OSMF) user name, which can be the same as your TSO login." + }, + "password": { + "type": "string", + "description": "Mainframe (z/OSMF) password, which can be the same as your TSO password." + }, + "rejectUnauthorized": { + "type": "boolean", + "description": "Reject self-signed certificates.", + "default": true + }, + "certFile": { + "type": "string", + "description": "The file path to a certificate file to use for authentication" + }, + "certKeyFile": { + "type": "string", + "description": "The file path to a certificate key file to use for authentication" + }, + "basePath": { + "type": "string", + "description": "The base path for your API mediation layer instance. Specify this option to prepend the base path to all z/OSMF resources when making REST requests. Do not specify this option if you are not using an API mediation layer." + }, + "protocol": { + "type": "string", + "description": "The protocol used (HTTP or HTTPS)", + "default": "https", + "enum": [ + "http", + "https" + ] + }, + "encoding": { + "type": "string", + "description": "The encoding for download and upload of z/OS data set and USS files. The default encoding if not specified is IBM-1047." + }, + "responseTimeout": { + "type": "number", + "description": "The maximum amount of time in seconds the z/OSMF Files TSO servlet should run before returning a response. Any request exceeding this amount of time will be terminated and return an error. Allowed values: 5 - 600" + } + }, + "required": [] + }, + "secure": { + "items": { + "enum": [ + "user", + "password" + ] + } + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "tso" + } + } + }, + "then": { + "properties": { + "properties": { + "type": "object", + "title": "TSO Profile", + "description": "z/OS TSO/E User Profile", + "properties": { + "account": { + "type": "string", + "description": "Your z/OS TSO/E accounting information." + }, + "characterSet": { + "type": "string", + "description": "Character set for address space to convert messages and responses from UTF-8 to EBCDIC.", + "default": "697" + }, + "codePage": { + "type": "string", + "description": "Codepage value for TSO/E address space to convert messages and responses from UTF-8 to EBCDIC.", + "default": "1047" + }, + "columns": { + "type": "number", + "description": "The number of columns on a screen.", + "default": 80 + }, + "logonProcedure": { + "type": "string", + "description": "The logon procedure to use when creating TSO procedures on your behalf.", + "default": "IZUFPROC" + }, + "regionSize": { + "type": "number", + "description": "Region size for the TSO/E address space.", + "default": 4096 + }, + "rows": { + "type": "number", + "description": "The number of rows on a screen.", + "default": 24 + } + }, + "required": [] + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "ssh" + } + } + }, + "then": { + "properties": { + "properties": { + "type": "object", + "title": "z/OS SSH Profile", + "description": "z/OS SSH Profile", + "properties": { + "host": { + "type": "string", + "description": "The z/OS SSH server host name." + }, + "port": { + "type": "number", + "description": "The z/OS SSH server port.", + "default": 22 + }, + "user": { + "type": "string", + "description": "Mainframe user name, which can be the same as your TSO login." + }, + "password": { + "type": "string", + "description": "Mainframe password, which can be the same as your TSO password." + }, + "privateKey": { + "type": "string", + "description": "Path to a file containing your private key, that must match a public key stored in the server for authentication" + }, + "keyPassphrase": { + "type": "string", + "description": "Private key passphrase, which unlocks the private key." + }, + "handshakeTimeout": { + "type": "number", + "description": "How long in milliseconds to wait for the SSH handshake to complete." + } + }, + "required": [] + }, + "secure": { + "items": { + "enum": [ + "user", + "password", + "keyPassphrase" + ] + } + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "base" + } + } + }, + "then": { + "properties": { + "properties": { + "type": "object", + "title": "Base Profile", + "description": "Base profile that stores values shared by multiple service profiles", + "properties": { + "host": { + "type": "string", + "description": "Host name of service on the mainframe." + }, + "port": { + "type": "number", + "description": "Port number of service on the mainframe." + }, + "user": { + "type": "string", + "description": "User name to authenticate to service on the mainframe." + }, + "password": { + "type": "string", + "description": "Password to authenticate to service on the mainframe." + }, + "rejectUnauthorized": { + "type": "boolean", + "description": "Reject self-signed certificates.", + "default": true + }, + "tokenType": { + "type": "string", + "description": "The type of token to get and use for the API. Omit this option to use the default token type, which is provided by 'zowe auth login'." + }, + "tokenValue": { + "type": "string", + "description": "The value of the token to pass to the API." + }, + "certFile": { + "type": "string", + "description": "The file path to a certificate file to use for authentication.\n\nNote: The CLI does not support certificate files that require a password. For more information, search Troubleshooting PEM Certificates in Zowe Docs." + }, + "certKeyFile": { + "type": "string", + "description": "The file path to a certificate key file to use for authentication" + } + }, + "required": [] + }, + "secure": { + "items": { + "enum": [ + "user", + "password", + "tokenValue" + ] + } + } + } + } + } + ] + } + } + }, + "defaults": { + "type": "object", + "description": "Mapping of profile types to default profile names", + "properties": { + "zosmf": { + "description": "Default zosmf profile", + "type": "string" + }, + "tso": { + "description": "Default tso profile", + "type": "string" + }, + "ssh": { + "description": "Default ssh profile", + "type": "string" + }, + "base": { + "description": "Default base profile", + "type": "string" + } + } + }, + "autoStore": { + "type": "boolean", + "description": "If true, values you enter when prompted are stored for future use" + } + } +} \ No newline at end of file diff --git a/src/test/kotlin/org/zowe/explorer/config/ZoweConfigTestSpec.kt b/src/test/kotlin/org/zowe/explorer/config/ZoweConfigTestSpec.kt new file mode 100644 index 000000000..1286e2bea --- /dev/null +++ b/src/test/kotlin/org/zowe/explorer/config/ZoweConfigTestSpec.kt @@ -0,0 +1,202 @@ +package org.zowe.explorer.config + +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFileManager +import io.kotest.matchers.equals.shouldBeEqual +import io.kotest.matchers.shouldBe +import io.mockk.* +import org.apache.commons.io.FileUtils +import org.zowe.explorer.config.connect.ConnectionConfig +import org.zowe.explorer.config.connect.ui.zosmf.ConnectionDialogState +import org.zowe.explorer.testutils.WithApplicationShouldSpec +import org.zowe.explorer.utils.crudable.getAll +import org.zowe.explorer.zowe.ZOWE_CONFIG_NAME +import org.zowe.explorer.zowe.service.ZoweConfigService +import org.zowe.explorer.zowe.service.ZoweConfigServiceImpl +import org.zowe.explorer.zowe.service.ZoweConfigState +import org.zowe.kotlinsdk.zowe.config.KeytarWrapper +import org.zowe.kotlinsdk.zowe.config.ZoweConfig +import java.io.File +import java.net.URL +import java.nio.charset.Charset +import java.nio.file.Files +import java.nio.file.Paths +import kotlin.reflect.KFunction + +class ZoweConfigTestSpec : WithApplicationShouldSpec({ + + val baseProjPath = System.getProperty("java.io.tmpdir") + val tmpZoweConfFile = baseProjPath + File.separator + ZOWE_CONFIG_NAME + val connectionDialogState = ConnectionDialogState( + connectionName = "a", + connectionUrl = "https://111.111.111.111:555", + username = "testUser", + password = "testPass", + isAllowSsl = true + ) + + afterSpec { + clearAllMocks() + } + + context("config module: zowe config file") { + + val mockedProject = mockk() + every { mockedProject.basePath } returns baseProjPath + every { mockedProject.name } returns "testProj" + + val copyURLToFileMock: ( + URL?, File? + ) -> Unit = FileUtils::copyURLToFile + mockkStatic(copyURLToFileMock as KFunction<*>) + every { + copyURLToFileMock(any() as URL, any() as File) + } just Runs + + mockkObject(ZoweConfigServiceImpl(mockedProject)) { + every { ZoweConfigService.getInstance(mockedProject) } returns ZoweConfigServiceImpl(mockedProject) + } + + mockkObject(ZoweConfigServiceImpl) + every { ZoweConfigServiceImpl.Companion["getResourceContent"](any() as String, any() as Charset) } answers { + String( + Files.readAllBytes( + Paths.get( + this::class.java.classLoader?.getResource(firstArg())?.path.toString() + ) + ), secondArg() + ) + } + + var checkFilePath = false + var checkUser = false + var checkPass = false + fun checkSaveNewSecProp( + filePath: String, + configCredentialsMap: MutableMap + ) { + if (filePath == tmpZoweConfFile) + checkFilePath = true + configCredentialsMap.forEach { + if (it.key == "profiles.base.properties.user" && it.value == "testUser") + checkUser = true + if (it.key == "profiles.base.properties.password" && it.value == "testPass") + checkPass = true + } + } + mockkObject(ZoweConfig) + every { + ZoweConfig.saveNewSecureProperties( + any() as String, + any() as MutableMap, any() + ) + } answers { + checkSaveNewSecProp(firstArg(), secondArg>()) + } + + val confMap = mutableMapOf>() + val configCredentialsMap = mutableMapOf() + configCredentialsMap["profiles.base.properties.user"] = "testUser" + configCredentialsMap["profiles.base.properties.password"] = "testPass" + confMap[tmpZoweConfFile] = configCredentialsMap + + every { ZoweConfig.Companion["readZoweCredentialsFromStorage"](any() as KeytarWrapper) } returns confMap + + val zoweConnConf = connectionDialogState.connectionConfig + zoweConnConf.zoweConfigPath = tmpZoweConfFile + zoweConnConf.name = "zowe-testProj" + + + should("add zowe team config file") { + + Files.deleteIfExists(Paths.get(tmpZoweConfFile)) + + var isPortAdded = false + var isHostAdded = false + var isSslAdded = false + + ApplicationManager.getApplication().invokeAndWait { + ZoweConfigService.getInstance(mockedProject).addZoweConfigFile(connectionDialogState) + VirtualFileManager.getInstance().syncRefresh() + } + + val read = Files.readAllLines(Paths.get(tmpZoweConfFile)) + for (listItem in read) { + if (listItem.contains("\"port\": 555")) + isPortAdded = true + if (listItem.contains("\"host\": \"111.111.111.111\"")) + isHostAdded = true + if (listItem.contains("\"rejectUnauthorized\": false")) + isSslAdded = true + } + + isPortAdded shouldBe true + isHostAdded shouldBe true + isSslAdded shouldBe true + checkFilePath shouldBe true + checkUser shouldBe true + checkPass shouldBe true + + } + + should("get zowe team config state") { + + val run1 = ZoweConfigService.getInstance(mockedProject).getZoweConfigState(false) + run1 shouldBeEqual ZoweConfigState.NOT_EXISTS + + val run2 = ZoweConfigService.getInstance(mockedProject).getZoweConfigState() + run2 shouldBeEqual ZoweConfigState.NEED_TO_ADD + + ConfigService.instance.crudable.addOrUpdate(zoweConnConf) + + val run3 = ZoweConfigService.getInstance(mockedProject).getZoweConfigState() + run3 shouldBeEqual ZoweConfigState.NEED_TO_UPDATE + + confMap[tmpZoweConfFile]?.set("profiles.base.properties.password", "testPassword") + + val run4 = ZoweConfigService.getInstance(mockedProject).getZoweConfigState() + run4 shouldBeEqual ZoweConfigState.SYNCHRONIZED + + } + + should("add or update zowe team config connection") { + + zoweConnConf.url = "222.222.222.222:666" + ConfigService.instance.crudable.addOrUpdate(zoweConnConf) + ConfigService.instance.crudable.getAll().toList() + .filter { it.name == zoweConnConf.name }[0].url shouldBeEqual zoweConnConf.url + + ZoweConfigService.getInstance(mockedProject).addOrUpdateZoweConfig(scanProject = true, checkConnection = false) + + ConfigService.instance.crudable.getAll().toList() + .filter { it.name == zoweConnConf.name }[0].url shouldBeEqual "https://${ + ZoweConfigService.getInstance( + mockedProject + ).zoweConfig?.host + }:${ZoweConfigService.getInstance(mockedProject).zoweConfig?.port}" + + } + + should("delete zowe team config connection") { + + Files.deleteIfExists(Paths.get(tmpZoweConfFile)) + + ConfigService.instance.crudable.getAll().toList() + .filter { it.name == zoweConnConf.name }[0].url shouldBeEqual "https://${ + ZoweConfigService.getInstance( + mockedProject + ).zoweConfig?.host + }:${ZoweConfigService.getInstance(mockedProject).zoweConfig?.port}" + + ZoweConfigService.getInstance(mockedProject).deleteZoweConfig() + + ConfigService.instance.crudable.getAll().toList() + .filter { it.name == zoweConnConf.name }.size shouldBeEqual 0 + + } + + } + +}) +