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 27d6d58b..5b14c486 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 @@ -14,6 +14,9 @@ package org.zowe.explorer.config.connect.ui.zosmf +import com.google.gson.JsonSyntaxException +import com.intellij.ide.DataManager +import com.intellij.openapi.actionSystem.* import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.runInEdt import com.intellij.openapi.options.BoundSearchableConfigurable @@ -34,6 +37,7 @@ import org.zowe.explorer.config.connect.Credentials import org.zowe.explorer.config.ws.FilesWorkingSetConfig import org.zowe.explorer.config.ws.JesWorkingSetConfig import org.zowe.explorer.config.ws.WorkingSetConfig +import org.zowe.explorer.telemetry.NotificationsService import org.zowe.explorer.utils.crudable.getAll import org.zowe.explorer.utils.isThe import org.zowe.explorer.utils.runWriteActionInEdtAndWait @@ -109,7 +113,16 @@ class ZOSMFConnectionConfigurable : BoundSearchableConfigurable("z/OSMF Connecti return } - val zoweConfig = parseConfigJson(configFile.inputStream) + val zoweConfig = try { + parseConfigJson(configFile.inputStream) + } catch (e: JsonSyntaxException) { + NotificationsService.errorNotification( + e, + project = DataManager.getInstance().getDataContext(panel).getData(PlatformDataKeys.PROJECT), + custTitle = "Error with Zowe config file" + ) + return + } zoweConfig.extractSecureProperties(configFile.path.split("/").toTypedArray()) kotlin.runCatching { zoweConfig.updateFromState(state) diff --git a/src/main/kotlin/org/zowe/explorer/zowe/actions/UpdateZoweConfigAction.kt b/src/main/kotlin/org/zowe/explorer/zowe/actions/UpdateZoweConfigAction.kt index 7ef55583..3cfc2b24 100644 --- a/src/main/kotlin/org/zowe/explorer/zowe/actions/UpdateZoweConfigAction.kt +++ b/src/main/kotlin/org/zowe/explorer/zowe/actions/UpdateZoweConfigAction.kt @@ -14,6 +14,7 @@ package org.zowe.explorer.zowe.actions +import com.google.gson.JsonSyntaxException import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.CommonDataKeys @@ -84,12 +85,17 @@ class UpdateZoweConfigAction : DumbAwareAction() { zoweConfigService.localZoweConfig else zoweConfigService.globalZoweConfig - if (type == ZoweConfigType.LOCAL) { - zoweConfigService.localZoweConfig = parseConfigJson(editor.document.text) - zoweConfigService.localZoweConfig?.extractSecureProperties(vFile.path.split("/").toTypedArray()) - } else { - zoweConfigService.globalZoweConfig = parseConfigJson(editor.document.text) - zoweConfigService.globalZoweConfig?.extractSecureProperties(vFile.path.split("/").toTypedArray()) + try { + if (type == ZoweConfigType.LOCAL) { + zoweConfigService.localZoweConfig = parseConfigJson(editor.document.text) + zoweConfigService.localZoweConfig?.extractSecureProperties(vFile.path.split("/").toTypedArray()) + } else { + zoweConfigService.globalZoweConfig = parseConfigJson(editor.document.text) + zoweConfigService.globalZoweConfig?.extractSecureProperties(vFile.path.split("/").toTypedArray()) + } + } catch (ex: JsonSyntaxException) { + e.presentation.isEnabledAndVisible = false + return } val zoweState = zoweConfigService.getZoweConfigState(false, type = type) e.presentation.isEnabledAndVisible = 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 0540e327..64a359f5 100644 --- a/src/main/kotlin/org/zowe/explorer/zowe/service/ZoweConfigServiceImpl.kt +++ b/src/main/kotlin/org/zowe/explorer/zowe/service/ZoweConfigServiceImpl.kt @@ -14,10 +14,8 @@ package org.zowe.explorer.zowe.service -import com.intellij.notification.Notification 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.runReadAction import com.intellij.openapi.application.runWriteAction @@ -38,6 +36,7 @@ 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.telemetry.NotificationsService import org.zowe.explorer.utils.crudable.find import org.zowe.explorer.utils.crudable.getAll import org.zowe.explorer.utils.runTask @@ -64,9 +63,6 @@ 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" - const val ZOWE_PROJECT_PREFIX = "zowe-" /** @@ -123,22 +119,6 @@ class ZoweConfigServiceImpl(override val myProject: Project) : ZoweConfigService override var globalZoweConfig: ZoweConfig? = null - /** - * Displays an error notification if an error was received. - * @param t thrown error. - * @param title error text. - */ - private fun notifyError(t: Throwable, title: String? = null) { - Notifications.Bus.notify( - Notification( - ZOWE_CONFIG_NOTIFICATION_GROUP_ID, - title ?: "Error with Zowe config file", - t.message ?: t.toString(), - NotificationType.ERROR - ) - ) - } - /** * Checks project contains zowe.config.json. If zowe config presented * it will parse it and save to object model inside zoweConfig field. @@ -162,7 +142,8 @@ class ZoweConfigServiceImpl(override val myProject: Project) : ZoweConfigService } } } catch (e: Exception) { - throw Exception("Cannot parse $type Zowe config file") + NotificationsService.errorNotification(e, project = myProject, custTitle="Error with Zowe config file") + return null } } @@ -208,8 +189,8 @@ class ZoweConfigServiceImpl(override val myProject: Project) : ZoweConfigService /** * Added notification about connection failure with action of force connection adding. - * @param title - notification title. - * @param content - notification content. + * @param title notification title. + * @param content notification content. * @return Nothing. */ private fun notifyUiOnConnectionFailure(title: String, content: String, type: ZoweConfigType) { @@ -277,27 +258,23 @@ class ZoweConfigServiceImpl(override val myProject: Project) : ZoweConfigService this.globalZoweConfig zoweConfig ?: throw Exception("Cannot get $type Zowe config") - var allPreparedConn = mutableListOf() - if (checkConnection) { - val failedURLs = mutableListOf() - allPreparedConn = testAndPrepareAllZosmfConnections(zoweConfig, type, failedURLs) - if (failedURLs.isNotEmpty()) { - val andMore = if (failedURLs.size > 3) - "..." - else - "" - notifyUiOnConnectionFailure( - "Connection failed to:", - "${failedURLs.joinToString(separator = ",

")} ${andMore}", - type - ) - return - } + val (allPreparedConn, failedConnections) = testAndPrepareAllZosmfConnections(zoweConfig, type) + if (checkConnection and failedConnections.isNotEmpty()) { + val andMore = if (failedConnections.size > 3) "..." else "" + notifyUiOnConnectionFailure( + "Connection failed to:", + "${failedConnections.map{it.url}.joinToString(separator = ",

")} $andMore", + type + ) } - for (zosmfConnection in allPreparedConn) { + val conToAdd = if (checkConnection) + allPreparedConn.subtract(failedConnections.toSet()) + else + allPreparedConn + conToAdd.forEach { zosmfConnection -> val connectionOpt = configCrudable.addOrUpdate(zosmfConnection) if (!connectionOpt.isEmpty) { - var topic = if (type == ZoweConfigType.LOCAL) + val topic = if (type == ZoweConfigType.LOCAL) LOCAL_ZOWE_CONFIG_CHANGED else GLOBAL_ZOWE_CONFIG_CHANGED @@ -306,7 +283,7 @@ class ZoweConfigServiceImpl(override val myProject: Project) : ZoweConfigService } } catch (e: Exception) { - notifyError(e) + NotificationsService.errorNotification(e, project = myProject, custTitle="Error with Zowe config file") } } @@ -319,9 +296,9 @@ class ZoweConfigServiceImpl(override val myProject: Project) : ZoweConfigService private fun prepareConnection(zosmfConnection: ZOSConnection, type: ZoweConfigType): ConnectionConfig { val username = zosmfConnection.user val password = zosmfConnection.password - val zoweConnection = findExistingConnection(type, zosmfConnection.profileName)?.let { - zosmfConnection.toConnectionConfig(it.uuid, it.zVersion, type = type) - } ?: zosmfConnection.toConnectionConfig(UUID.randomUUID().toString(), type = type) + val zoweConnection = findExistingConnection(type, zosmfConnection.profileName) + ?.let { zosmfConnection.toConnectionConfig(it.uuid, it.zVersion, type = type) } + ?: zosmfConnection.toConnectionConfig(UUID.randomUUID().toString(), type = type) CredentialService.getService().setCredentials(zoweConnection.uuid, username, password) return zoweConnection } @@ -330,24 +307,25 @@ class ZoweConfigServiceImpl(override val myProject: Project) : ZoweConfigService * Convert all zosmf connections from zowe config file to ConnectionConfig and tests them * @param zoweConfig * @param type of zowe config - * @return list of URLs which did not pass the test + * @return pair of lists, one is the connections list, the second is the list of URLs which did not pass the test */ private fun testAndPrepareAllZosmfConnections( zoweConfig: ZoweConfig, - type: ZoweConfigType, - failedURLs: MutableList - ): MutableList { - var allPreparedConn = mutableListOf() - for (zosmfConnection in zoweConfig.getListOfZosmfConections()) { - val zoweConnection = prepareConnection(zosmfConnection, type) - try { - testAndPrepareConnection(zoweConnection) - } catch (t: Throwable) { - failedURLs.add(zoweConnection.url) + type: ZoweConfigType + ): Pair, List> { + return zoweConfig.getListOfZosmfConections() + .fold( + mutableListOf() to mutableListOf() + ) { (allConnectionConfigs, failedURLs), zosmfConnection -> + val zoweConnection = prepareConnection(zosmfConnection, type) + try { + testAndPrepareConnection(zoweConnection) + } catch (t: Throwable) { + failedURLs.add(zoweConnection) + } + allConnectionConfigs.add(zoweConnection) + allConnectionConfigs to failedURLs } - allPreparedConn.add(zoweConnection) - } - return allPreparedConn } /** @@ -384,7 +362,7 @@ class ZoweConfigServiceImpl(override val myProject: Project) : ZoweConfigService } } catch (e: Exception) { - notifyError(e) + NotificationsService.errorNotification(e, project = myProject, custTitle="Error with Zowe config file") } } @@ -434,18 +412,20 @@ class ZoweConfigServiceImpl(override val myProject: Project) : ZoweConfigService val allConnections = configCrudable.getAll().toList() val allConnectionsNames: MutableList = allConnections.map { it.name }.toMutableList() - allConnections.filter { it.zoweConfigPath == getZoweConfigLocation(myProject, type) }.forEach { - var index = 1 - var newName = it.name - while (allConnectionsNames.contains(newName)) { - newName = it.name.plus(index.toString()) - index++ + allConnections + .filter { it.zoweConfigPath == getZoweConfigLocation(myProject, type) } + .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) } - allConnectionsNames.add(newName) - it.name = newName - it.zoweConfigPath = null - configCrudable.update(it) - } } private fun createZoweSchemaJsonIfNotExists() { @@ -490,11 +470,7 @@ class ZoweConfigServiceImpl(override val myProject: Project) : ZoweConfigService */ override fun getZoweConfigState(scanProject: Boolean, type: ZoweConfigType): ZoweConfigState { if (scanProject) { - try { scanForZoweConfig(type) - } catch (e: Exception) { - notifyError(e) - } } val zoweConfig = if (type == ZoweConfigType.LOCAL) localZoweConfig ?: return ZoweConfigState.NOT_EXISTS @@ -502,39 +478,37 @@ class ZoweConfigServiceImpl(override val myProject: Project) : ZoweConfigService globalZoweConfig ?: return ZoweConfigState.NOT_EXISTS findAllZosmfExistingConnection(type) ?: return ZoweConfigState.NEED_TO_ADD - var ret = ZoweConfigState.SYNCHRONIZED - - for (zosConnection in zoweConfig.getListOfZosmfConections()) { - val existingConnection = - findExistingConnection(type, zosConnection.profileName) - if (existingConnection == null) - ret = setZoweConfigState(ret, ZoweConfigState.NEED_TO_ADD) - else { - val newConnectionList = zoweConfig.getListOfZosmfConections() - .filter { it.profileName == getProfileNameFromConnName(existingConnection.name) } - val newConnection = if (newConnectionList.isNotEmpty()) { - newConnectionList[0].toConnectionConfig( - existingConnection.uuid.toString(), existingConnection.zVersion, existingConnection.owner, type = type - ) - } else { - ret = setZoweConfigState(ret, ZoweConfigState.NEED_TO_ADD) - continue - } - val zoweUsername = zosConnection.user - val zowePassword = zosConnection.password - - ret = if ( - existingConnection == newConnection - && CredentialService.getUsername(newConnection) == zoweUsername - && CredentialService.getPassword(newConnection) == zowePassword - ) { - setZoweConfigState(ret, ZoweConfigState.SYNCHRONIZED) + + return zoweConfig + .getListOfZosmfConections() + .fold(ZoweConfigState.SYNCHRONIZED) { prevZoweConfigState, zosConnection -> + val existingConnection = findExistingConnection(type, zosConnection.profileName) + val currZoweConfigState = if (existingConnection == null) { + ZoweConfigState.NEED_TO_ADD } else { - setZoweConfigState(ret, ZoweConfigState.NEED_TO_UPDATE) + val newConnectionList = zoweConfig.getListOfZosmfConections() + .filter { it.profileName == getProfileNameFromConnName(existingConnection.name) } + if (newConnectionList.isNotEmpty()) { + val newConnection = newConnectionList[0].toConnectionConfig( + existingConnection.uuid, existingConnection.zVersion, existingConnection.owner, type = type + ) + val zoweUsername = zosConnection.user + val zowePassword = zosConnection.password + if ( + existingConnection == newConnection + && CredentialService.getUsername(newConnection) == zoweUsername + && CredentialService.getPassword(newConnection) == zowePassword + ) { + ZoweConfigState.SYNCHRONIZED + } else { + ZoweConfigState.NEED_TO_UPDATE + } + } else { + ZoweConfigState.NEED_TO_ADD + } } + setZoweConfigState(prevZoweConfigState, currZoweConfigState) } - } - return ret } /** diff --git a/src/test/kotlin/org/zowe/explorer/config/ZoweConfigTestSpec.kt b/src/test/kotlin/org/zowe/explorer/config/ZoweConfigTestSpec.kt index b82b976f..cf3b0003 100644 --- a/src/test/kotlin/org/zowe/explorer/config/ZoweConfigTestSpec.kt +++ b/src/test/kotlin/org/zowe/explorer/config/ZoweConfigTestSpec.kt @@ -41,8 +41,10 @@ import org.zowe.explorer.dataops.operations.InfoOperation import org.zowe.explorer.dataops.operations.ZOSInfoOperation import org.zowe.explorer.explorer.Explorer import org.zowe.explorer.explorer.WorkingSet +import org.zowe.explorer.telemetry.NotificationsService import org.zowe.explorer.testutils.WithApplicationShouldSpec import org.zowe.explorer.testutils.testServiceImpl.TestDataOpsManagerImpl +import org.zowe.explorer.testutils.testServiceImpl.TestNotificationsServiceImpl import org.zowe.explorer.utils.crudable.* import org.zowe.explorer.utils.runIfTrue import org.zowe.explorer.utils.validateForBlank @@ -128,6 +130,21 @@ class ZoweConfigTestSpec : WithApplicationShouldSpec({ Optional.of(ConnectionConfig()) } + val notificationsService = NotificationsService.getService() as TestNotificationsServiceImpl + notificationsService.testInstance = object : TestNotificationsServiceImpl() { + override fun notifyError( + t: Throwable, + project: Project?, + custTitle: String?, + custDetailsShort: String?, + custDetailsLong: String? + ) { + if (custTitle == "Error with Zowe config file") { + notified = true + } + } + } + afterEach { isFilesWriteTriggered = false isRunWriteActionCalled = false @@ -329,7 +346,6 @@ class ZoweConfigTestSpec : WithApplicationShouldSpec({ isInputStreamCalled shouldBe true isReturnedZoweConfig shouldBe true isScanForZoweConfigCalled shouldBe true - isZOSInfoCalled shouldBe false } should("delete zowe team config connection") { @@ -360,20 +376,23 @@ class ZoweConfigTestSpec : WithApplicationShouldSpec({ j = true listOf(jWSConf).stream() } - var isShowOkCancelDialogCalled = false - val showOkCancelDialogMock: (String, String, String, String, Icon?) -> Int = ::showOkCancelDialog - mockkStatic(showOkCancelDialogMock as KFunction<*>) - every { - showOkCancelDialogMock(any(), any(), any(), any(), any()) - } answers { - isShowOkCancelDialogCalled = true - Messages.OK + val notificationsService = NotificationsService.getService() as TestNotificationsServiceImpl + notificationsService.testInstance = object : TestNotificationsServiceImpl() { + override fun notifyError( + t: Throwable, + project: Project?, + custTitle: String?, + custDetailsShort: String?, + custDetailsLong: String? + ) { + notified = true + } } mockedZoweConfigService.deleteZoweConfig(type = ZoweConfigType.LOCAL) f shouldBe true j shouldBe true - isShowOkCancelDialogCalled shouldBe true + notified shouldBe true isConnectionDeleted shouldBe false } @@ -724,4 +743,4 @@ class ZoweConfigTestSpec : WithApplicationShouldSpec({ } -}) \ No newline at end of file +}) diff --git a/src/test/kotlin/org/zowe/explorer/config/connect/ui/zosmf/ZOSMFConnectionConfigurableTest.kt b/src/test/kotlin/org/zowe/explorer/config/connect/ui/zosmf/ZOSMFConnectionConfigurableTest.kt index a0ccff45..283f9a3e 100644 --- a/src/test/kotlin/org/zowe/explorer/config/connect/ui/zosmf/ZOSMFConnectionConfigurableTest.kt +++ b/src/test/kotlin/org/zowe/explorer/config/connect/ui/zosmf/ZOSMFConnectionConfigurableTest.kt @@ -14,6 +14,9 @@ package org.zowe.explorer.config.connect.ui.zosmf +import com.intellij.ide.DataManager +import com.intellij.openapi.actionSystem.DataContext +import com.intellij.openapi.actionSystem.PlatformDataKeys import com.intellij.openapi.project.Project import com.intellij.openapi.ui.DialogWrapper import com.intellij.openapi.ui.Messages @@ -29,7 +32,9 @@ import org.zowe.explorer.common.ui.ValidatingTableView import org.zowe.explorer.config.ConfigStateV2 import org.zowe.explorer.config.connect.ConnectionConfig import org.zowe.explorer.config.makeCrudableWithoutListeners +import org.zowe.explorer.telemetry.NotificationsService import org.zowe.explorer.testutils.WithApplicationShouldSpec +import org.zowe.explorer.testutils.testServiceImpl.TestNotificationsServiceImpl import org.zowe.kotlinsdk.annotations.ZVersion import org.zowe.kotlinsdk.zowe.config.DefaultKeytarWrapper import org.zowe.kotlinsdk.zowe.config.KeytarWrapper @@ -49,6 +54,7 @@ class ZOSMFConnectionConfigurableTest : WithApplicationShouldSpec({ var isShowOkCancelDialogCalled = false var isFindFileByNioPathCalled = false var isInputStreamCalled = false + var notified = false afterSpec { clearAllMocks() @@ -59,10 +65,26 @@ class ZOSMFConnectionConfigurableTest : WithApplicationShouldSpec({ isShowOkCancelDialogCalled = false isFindFileByNioPathCalled = false isInputStreamCalled = false + notified = false } context("ZOSMFConnectionConfigurable:") { + val notificationsService = NotificationsService.getService() as TestNotificationsServiceImpl + notificationsService.testInstance = object : TestNotificationsServiceImpl() { + override fun notifyError( + t: Throwable, + project: Project?, + custTitle: String?, + custDetailsShort: String?, + custDetailsLong: String? + ) { + if (custTitle == "Error with Zowe config file") { + notified = true + } + } + } + val state = ConnectionDialogState( connectionUuid = "0000", connectionUrl = "https://111.111.111.111:111", @@ -156,6 +178,42 @@ class ZOSMFConnectionConfigurableTest : WithApplicationShouldSpec({ isFindFileByNioPathCalled = true vfMock } + every { vfMock.inputStream } answers { + isInputStreamCalled = true + val fileCont = "{\n" + + " \"\$schema\": \"./zowe.schema.json\",\n" + + " \"profiles\": {\n" + + " \"zosmf\": {\n" + + "}" + fileCont.toByteArray().inputStream() + } + every { vfMock.path } returns "/zowe/file/path/zowe.config.json" + every { vfMock.charset } returns Charsets.UTF_8 + every { vfMock.setBinaryContent(any()) } just Runs + + mockkObject(ZoweConfig) + val confMap = mutableMapOf>() + val configCredentialsMap = mutableMapOf() + configCredentialsMap["profiles.base.properties.user"] = "testUser" + configCredentialsMap["profiles.base.properties.password"] = "testPass" + confMap.clear() + confMap["/zowe/file/path/zowe.config.json"] = configCredentialsMap + every { ZoweConfig.Companion["readZoweCredentialsFromStorage"](any()) } returns confMap + + should("updateZoweConfigIfNeeded throw JsonSyntaxException") { + zOSMFConnectionConfigurableMock::class.declaredMemberFunctions.find { it.name == "updateZoweConfigIfNeeded" } + ?.let { + it.isAccessible = true + try { + it.call(zOSMFConnectionConfigurableMock, state) + } catch (t: Throwable) { + println("ghjkk") + t.cause.toString().shouldContain("Zowe config file not found") + } + } + notified shouldBe true + } + every { vfMock.inputStream } answers { isInputStreamCalled = true val fileCont = "{\n" + @@ -205,18 +263,6 @@ class ZOSMFConnectionConfigurableTest : WithApplicationShouldSpec({ "}" fileCont.toByteArray().inputStream() } - every { vfMock.path } returns "/zowe/file/path/zowe.config.json" - every { vfMock.charset } returns Charsets.UTF_8 - every { vfMock.setBinaryContent(any()) } just Runs - - mockkObject(ZoweConfig) - val confMap = mutableMapOf>() - val configCredentialsMap = mutableMapOf() - configCredentialsMap["profiles.base.properties.user"] = "testUser" - configCredentialsMap["profiles.base.properties.password"] = "testPass" - confMap.clear() - confMap["/zowe/file/path/zowe.config.json"] = configCredentialsMap - every { ZoweConfig.Companion["readZoweCredentialsFromStorage"](any()) } returns confMap should("updateZoweConfigIfNeeded success") { zOSMFConnectionConfigurableMock::class.declaredMemberFunctions.find { it.name == "updateZoweConfigIfNeeded" }