From 3c629a9e78f6c4514efd695fc8d6efaf620011e2 Mon Sep 17 00:00:00 2001 From: Arseni Tsikhamirau Date: Fri, 4 Oct 2024 10:37:34 +0200 Subject: [PATCH 01/15] IJMP-1821-Error-without-text --- .../formainframe/dataops/operations/InfoOperationRunner.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/eu/ibagroup/formainframe/dataops/operations/InfoOperationRunner.kt b/src/main/kotlin/eu/ibagroup/formainframe/dataops/operations/InfoOperationRunner.kt index 5b816eee6..803274092 100644 --- a/src/main/kotlin/eu/ibagroup/formainframe/dataops/operations/InfoOperationRunner.kt +++ b/src/main/kotlin/eu/ibagroup/formainframe/dataops/operations/InfoOperationRunner.kt @@ -57,7 +57,11 @@ class InfoOperationRunner : OperationRunner { .cancelByIndicator(progressIndicator) .execute() if (!response.isSuccessful) { - val headMessage = responseMessageMap[response.message()] ?: response.message() + log.info("Test connection response: $response") + val zosmfMessage = response.message().trim() + val headMessage = if (zosmfMessage.isNotEmpty()) + responseMessageMap[zosmfMessage] ?: zosmfMessage + else responseMessageMap["Unauthorized"] ?: zosmfMessage throw CallException(response, headMessage) } return response.body() ?: throw CallException(response, "Cannot parse z/OSMF info request body") From 6aaf9db2c1b9023edc339de7b2bafd82f39cc7eb Mon Sep 17 00:00:00 2001 From: Arseni Tsikhamirau Date: Fri, 4 Oct 2024 10:38:22 +0200 Subject: [PATCH 02/15] IJMP-1963-NPE-during-sorting --- .../formainframe/explorer/ui/DSMaskNode.kt | 4 ++-- .../formainframe/explorer/ui/JesFilterNode.kt | 14 +++++++------- .../formainframe/explorer/ui/LibraryNode.kt | 6 +++--- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/main/kotlin/eu/ibagroup/formainframe/explorer/ui/DSMaskNode.kt b/src/main/kotlin/eu/ibagroup/formainframe/explorer/ui/DSMaskNode.kt index 0071646a8..92a1d64cf 100644 --- a/src/main/kotlin/eu/ibagroup/formainframe/explorer/ui/DSMaskNode.kt +++ b/src/main/kotlin/eu/ibagroup/formainframe/explorer/ui/DSMaskNode.kt @@ -152,9 +152,9 @@ class DSMaskNode( return { val datasetAttributes = when (it) { is FileLikeDatasetNode -> DataOpsManager.getService() - .tryToGetAttributes(it.virtualFile) as RemoteDatasetAttributes + .tryToGetAttributes(it.virtualFile) as? RemoteDatasetAttributes - is LibraryNode -> DataOpsManager.getService().tryToGetAttributes(it.virtualFile) as RemoteDatasetAttributes + is LibraryNode -> DataOpsManager.getService().tryToGetAttributes(it.virtualFile) as? RemoteDatasetAttributes else -> null } when (key) { diff --git a/src/main/kotlin/eu/ibagroup/formainframe/explorer/ui/JesFilterNode.kt b/src/main/kotlin/eu/ibagroup/formainframe/explorer/ui/JesFilterNode.kt index 51e03080b..cb83fe9e3 100644 --- a/src/main/kotlin/eu/ibagroup/formainframe/explorer/ui/JesFilterNode.kt +++ b/src/main/kotlin/eu/ibagroup/formainframe/explorer/ui/JesFilterNode.kt @@ -125,14 +125,14 @@ class JesFilterNode( private fun selector(key: SortQueryKeys): (AbstractTreeNode<*>) -> String? { return { val jobInfo = - (DataOpsManager.getService().tryToGetAttributes((it as JobNode).value) as RemoteJobAttributes).jobInfo + (DataOpsManager.getService().tryToGetAttributes((it as JobNode).value) as? RemoteJobAttributes)?.jobInfo when (key) { - SortQueryKeys.JOB_NAME -> jobInfo.jobName - SortQueryKeys.JOB_OWNER -> jobInfo.owner - SortQueryKeys.JOB_STATUS -> jobInfo.status?.value - SortQueryKeys.JOB_ID -> jobInfo.jobId - SortQueryKeys.JOB_CREATION_DATE -> jobInfo.execStarted - SortQueryKeys.JOB_COMPLETION_DATE -> jobInfo.execEnded + SortQueryKeys.JOB_NAME -> jobInfo?.jobName + SortQueryKeys.JOB_OWNER -> jobInfo?.owner + SortQueryKeys.JOB_STATUS -> jobInfo?.status?.value + SortQueryKeys.JOB_ID -> jobInfo?.jobId + SortQueryKeys.JOB_CREATION_DATE -> jobInfo?.execStarted + SortQueryKeys.JOB_COMPLETION_DATE -> jobInfo?.execEnded else -> null } } diff --git a/src/main/kotlin/eu/ibagroup/formainframe/explorer/ui/LibraryNode.kt b/src/main/kotlin/eu/ibagroup/formainframe/explorer/ui/LibraryNode.kt index 0cecf0863..9d2f9bafb 100644 --- a/src/main/kotlin/eu/ibagroup/formainframe/explorer/ui/LibraryNode.kt +++ b/src/main/kotlin/eu/ibagroup/formainframe/explorer/ui/LibraryNode.kt @@ -145,10 +145,10 @@ class LibraryNode( return { val memberInfo = (DataOpsManager.getService() - .tryToGetAttributes((it as FileLikeDatasetNode).virtualFile) as RemoteMemberAttributes).info + .tryToGetAttributes((it as FileLikeDatasetNode).virtualFile) as? RemoteMemberAttributes)?.info when (key) { - SortQueryKeys.MEMBER_NAME -> memberInfo.name - SortQueryKeys.MEMBER_MODIFICATION_DATE -> memberInfo.modificationDate + SortQueryKeys.MEMBER_NAME -> memberInfo?.name + SortQueryKeys.MEMBER_MODIFICATION_DATE -> memberInfo?.modificationDate else -> null } } From b36c486c14abf5be51b259387d5b37daa6f9531b Mon Sep 17 00:00:00 2001 From: Uladzislau Date: Fri, 4 Oct 2024 11:28:22 +0200 Subject: [PATCH 03/15] Small fix Signed-off-by: Uladzislau --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 761698463..7695126c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ All notable changes to the Zowe IntelliJ Plugin will be documented in this file. ## [Unreleased] +## [1.2.3-223] (2024-10-03) + ### Features * Feature: Correct alignment for fields in working set dialog ([745143cc](https://github.com/zowe/zowe-explorer-intellij/commit/745143cc)) @@ -498,6 +500,7 @@ All notable changes to the Zowe IntelliJ Plugin will be documented in this file. * Bugfix: GitHub issue #85: The windows 'Add Working Set'/'Edit Working Set' are automatically resized if z/OSMF connection with very long name is added +[1.2.3-223]: https://github.com/for-mainframe/For-Mainframe/compare/1.2.2-223...1.2.3-223 [1.2.2-223]: https://github.com/for-mainframe/For-Mainframe/compare/1.2.1-223...1.2.2-223 [1.2.1-223]: https://github.com/for-mainframe/For-Mainframe/compare/1.2.0-223...1.2.1-223 [1.2.0-223]: https://github.com/for-mainframe/For-Mainframe/compare/1.1.2-223...1.2.0-223 From bd0484cab333c63044b43d8c49ed7d6400f21fd0 Mon Sep 17 00:00:00 2001 From: Arseni Tsikhamirau Date: Tue, 8 Oct 2024 13:14:03 +0200 Subject: [PATCH 04/15] IJMP-1696-Refresh-causes-encoding-to-be-reset --- .../dataops/attributes/RemoteUssAttributes.kt | 8 ++++---- .../dataops/attributes/RemoteUssAttributesService.kt | 3 ++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/eu/ibagroup/formainframe/dataops/attributes/RemoteUssAttributes.kt b/src/main/kotlin/eu/ibagroup/formainframe/dataops/attributes/RemoteUssAttributes.kt index c3ab7eab7..1203f5e29 100644 --- a/src/main/kotlin/eu/ibagroup/formainframe/dataops/attributes/RemoteUssAttributes.kt +++ b/src/main/kotlin/eu/ibagroup/formainframe/dataops/attributes/RemoteUssAttributes.kt @@ -76,7 +76,8 @@ data class RemoteUssAttributes( val gid: Long? = null, val groupId: String? = null, val modificationTime: String? = null, - val symlinkTarget: String? = null + val symlinkTarget: String? = null, + var charset: Charset = DEFAULT_BINARY_CHARSET ) : MFRemoteFileAttributes, Copyable { /** @@ -98,7 +99,8 @@ data class RemoteUssAttributes( gid = ussFile.gid, groupId = ussFile.groupId, modificationTime = ussFile.modificationTime, - symlinkTarget = ussFile.target + symlinkTarget = ussFile.target, + charset = DEFAULT_BINARY_CHARSET ) /** @@ -163,8 +165,6 @@ data class RemoteUssAttributes( || mode == FileModeValue.READ_WRITE_EXECUTE.mode } - var charset: Charset = DEFAULT_BINARY_CHARSET - override var contentMode: XIBMDataType = XIBMDataType(XIBMDataType.Type.BINARY) override val isCopyPossible: Boolean diff --git a/src/main/kotlin/eu/ibagroup/formainframe/dataops/attributes/RemoteUssAttributesService.kt b/src/main/kotlin/eu/ibagroup/formainframe/dataops/attributes/RemoteUssAttributesService.kt index 3dc204056..95e7f9f02 100644 --- a/src/main/kotlin/eu/ibagroup/formainframe/dataops/attributes/RemoteUssAttributesService.kt +++ b/src/main/kotlin/eu/ibagroup/formainframe/dataops/attributes/RemoteUssAttributesService.kt @@ -84,7 +84,8 @@ class RemoteUssAttributesService( gid = newAttributes.gid, groupId = newAttributes.groupId, modificationTime = newAttributes.modificationTime, - symlinkTarget = newAttributes.symlinkTarget + symlinkTarget = newAttributes.symlinkTarget, + charset = oldAttributes.charset ) } From 67d75cda20eb73db0590b0b98d363e5bb85fc517 Mon Sep 17 00:00:00 2001 From: Uladzislau Kalesnikau Date: Tue, 8 Oct 2024 20:19:06 +0200 Subject: [PATCH 05/15] Gitlab artifact name setup try --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 1f934661a..53ad4d37d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -15,7 +15,7 @@ build-job: unzip "$ZIP_NAME_WITH_EXT" -d content mv content/* ../../built-plugin artifacts: - name: "for-mainframe-private" + name: "${CI_PROJECT_NAME}_${CI_COMMIT_REF_SLUG}_${CI_PIPELINE_ID}" paths: - built-plugin/ expire_in: 4 weeks From 0a409632e4772db6098e64ed6a071951abbe95e2 Mon Sep 17 00:00:00 2001 From: Uladzislau Kalesnikau Date: Mon, 14 Oct 2024 13:22:57 +0200 Subject: [PATCH 06/15] IJMP-1656 Fixed issue with IntelliJ Server Certificates usage --- .../ibagroup/formainframe/api/ZosmfApiImpl.kt | 155 +++++------ .../formainframe/api/ZosmfApiImplTestSpec.kt | 247 ++++++++++++++++++ 2 files changed, 325 insertions(+), 77 deletions(-) create mode 100644 src/test/kotlin/eu/ibagroup/formainframe/api/ZosmfApiImplTestSpec.kt diff --git a/src/main/kotlin/eu/ibagroup/formainframe/api/ZosmfApiImpl.kt b/src/main/kotlin/eu/ibagroup/formainframe/api/ZosmfApiImpl.kt index 463608baa..f7e086512 100755 --- a/src/main/kotlin/eu/ibagroup/formainframe/api/ZosmfApiImpl.kt +++ b/src/main/kotlin/eu/ibagroup/formainframe/api/ZosmfApiImpl.kt @@ -14,23 +14,19 @@ package eu.ibagroup.formainframe.api -import com.google.gson.GsonBuilder +import com.intellij.util.net.ssl.CertificateManager import eu.ibagroup.formainframe.config.connect.ConnectionConfig -import org.zowe.kotlinsdk.buildApi -import org.zowe.kotlinsdk.buildApiWithBytesConverter import okhttp3.ConnectionPool import okhttp3.ConnectionSpec import okhttp3.Dispatcher import okhttp3.OkHttpClient -import retrofit2.converter.gson.GsonConverterFactory -import retrofit2.converter.scalars.ScalarsConverterFactory +import org.zowe.kotlinsdk.buildApi +import org.zowe.kotlinsdk.buildApiWithBytesConverter import java.security.SecureRandom import java.security.cert.CertificateException import java.security.cert.X509Certificate import java.util.concurrent.TimeUnit -import javax.net.ssl.SSLContext -import javax.net.ssl.TrustManager -import javax.net.ssl.X509TrustManager +import javax.net.ssl.* /** * Class that implements z/OSMF API for sending requests. @@ -101,44 +97,8 @@ class ZosmfApiImpl : ZosmfApi { } } -private val gsonFactory = GsonConverterFactory.create(GsonBuilder().create()) -private val scalarsConverterFactory = ScalarsConverterFactory.create() - -/** - * Connection pool is initialized and the connection parameters are set. - */ -private fun OkHttpClient.Builder.addThreadPool(): OkHttpClient.Builder { - readTimeout(5, TimeUnit.MINUTES) - connectTimeout(5, TimeUnit.MINUTES) - connectionPool(ConnectionPool(100, 5, TimeUnit.MINUTES)) - dispatcher(Dispatcher().apply { - maxRequests = 100 - maxRequestsPerHost = 100 - }) - return this -} - -val unsafeOkHttpClient by lazy { buildUnsafeClient() } -val safeOkHttpClient: OkHttpClient by lazy { - OkHttpClient.Builder() - .setupClient() - .build() -} - -/** - * Setups http client. Adds the necessary headers. Configures connection specs. - */ -private fun OkHttpClient.Builder.setupClient(): OkHttpClient.Builder { - return addThreadPool() - .addInterceptor { - it.request().newBuilder().addHeader("X-CSRF-ZOSMF-HEADER", "").build().let { request -> - it.proceed(request) - } - }.connectionSpecs(mutableListOf(ConnectionSpec.MODERN_TLS, ConnectionSpec.COMPATIBLE_TLS, ConnectionSpec.CLEARTEXT)) -} - /** - * Returns [OkHttpClient] depending on whether self-signed certificates are allowed or not. + * Returns [OkHttpClient] depending on whether self-signed certificates are allowed or not * @param isAllowSelfSigned whether to allow self-signed certificates. * @return safe or unsafe [OkHttpClient] object. */ @@ -150,43 +110,84 @@ private fun getOkHttpClient(isAllowSelfSigned: Boolean): OkHttpClient { } } +private val unsafeOkHttpClient by lazy { buildUnsafeClient() } +private val safeOkHttpClient by lazy { buildSafeClient() } + /** - * Method for building an unsafe http client that allows the use of self-signed certificates. - * @throws RuntimeException if timeout is exceeded. - * @return unsafe [OkHttpClient] object. + * Build an unsafe HTTP client that will allow the use of self-signed certificates + * @return unsafe [OkHttpClient] object */ private fun buildUnsafeClient(): OkHttpClient { - return try { - val trustAllCerts: Array = arrayOf( - object : X509TrustManager { - @Throws(CertificateException::class) - override fun checkClientTrusted( - chain: Array?, - authType: String? - ) { - } + val trustAllCerts: Array = arrayOf( + object : X509TrustManager { + @Throws(CertificateException::class) + override fun checkClientTrusted( + chain: Array?, + authType: String? + ) { + } - @Throws(CertificateException::class) - override fun checkServerTrusted( - chain: Array?, - authType: String? - ) { - } + @Throws(CertificateException::class) + override fun checkServerTrusted( + chain: Array?, + authType: String? + ) { + } - override fun getAcceptedIssuers(): Array { - return arrayOf() - } + override fun getAcceptedIssuers(): Array { + return arrayOf() } + } + ) + val sslContext = SSLContext.getInstance("TLSv1.2") + sslContext.init(null, trustAllCerts, SecureRandom()) + val sslSocketFactory = sslContext.socketFactory + return OkHttpClient.Builder() + .sslSocketFactory(sslSocketFactory, trustAllCerts[0] as X509TrustManager) + .hostnameVerifier { _, _ -> true } + .setupClient() + .build() +} + +/** + * Build a safe HTTP client that will reuse all allowed secured trusted certificates across the client's system + * (as well as those uploaded into IntelliJ's server certificates store) + * @return safe [OkHttpClient] object + */ +private fun buildSafeClient(): OkHttpClient { + val trustManager = CertificateManager.getInstance().trustManager + val sslContext = CertificateManager.getInstance().sslContext + return OkHttpClient.Builder() + .sslSocketFactory(sslContext.socketFactory, trustManager) + .setupClient() + .build() +} + +/** Set up an HTTP client. Adds the necessary headers. Configures connection specs */ +private fun OkHttpClient.Builder.setupClient(): OkHttpClient.Builder { + return addThreadPool() + .addInterceptor { + it.request() + .newBuilder() + .addHeader("X-CSRF-ZOSMF-HEADER", "") + .build() + .let { request -> + it.proceed(request) + } + } + .connectionSpecs( + mutableListOf(ConnectionSpec.MODERN_TLS, ConnectionSpec.COMPATIBLE_TLS, ConnectionSpec.CLEARTEXT) ) - val sslContext = SSLContext.getInstance("TLSv1.2") - sslContext.init(null, trustAllCerts, SecureRandom()) - val sslSocketFactory = sslContext.socketFactory - val builder = OkHttpClient.Builder() - builder.sslSocketFactory(sslSocketFactory, trustAllCerts[0] as X509TrustManager) - builder.hostnameVerifier { _, _ -> true } - builder.setupClient() - builder.build() - } catch (e: Exception) { - throw RuntimeException(e) - } +} + +/** Connection pool is initialized and the connection parameters are set */ +private fun OkHttpClient.Builder.addThreadPool(): OkHttpClient.Builder { + readTimeout(5, TimeUnit.MINUTES) + connectTimeout(5, TimeUnit.MINUTES) + connectionPool(ConnectionPool(100, 5, TimeUnit.MINUTES)) + dispatcher(Dispatcher().apply { + maxRequests = 100 + maxRequestsPerHost = 100 + }) + return this } diff --git a/src/test/kotlin/eu/ibagroup/formainframe/api/ZosmfApiImplTestSpec.kt b/src/test/kotlin/eu/ibagroup/formainframe/api/ZosmfApiImplTestSpec.kt new file mode 100644 index 000000000..fca19a949 --- /dev/null +++ b/src/test/kotlin/eu/ibagroup/formainframe/api/ZosmfApiImplTestSpec.kt @@ -0,0 +1,247 @@ +/* + * Copyright (c) 2024 IBA Group. + * + * 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 + * + * Contributors: + * IBA Group + * Zowe Community + */ + +package eu.ibagroup.formainframe.api + +import com.intellij.util.net.ssl.CertificateManager +import com.intellij.util.net.ssl.ConfirmingTrustManager +import eu.ibagroup.formainframe.config.connect.ConnectionConfig +import io.kotest.assertions.assertSoftly +import io.kotest.core.spec.style.ShouldSpec +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.mockk.* +import okhttp3.* +import org.zowe.kotlinsdk.buildApi +import org.zowe.kotlinsdk.buildApiWithBytesConverter +import java.util.concurrent.TimeUnit +import javax.net.ssl.SSLContext +import javax.net.ssl.SSLSocketFactory +import javax.net.ssl.TrustManager +import javax.net.ssl.X509TrustManager +import kotlin.reflect.KFunction + +class ZosmfApiImplTestSpec : ShouldSpec({ + afterSpec { + unmockkAll() + clearAllMocks() + } + + context("api module: ZosmfApiImpl") { + var sslFactoryActual: SSLSocketFactory? = null + var trustManagerActual: TrustManager? = null + + val safeTrustManagerMock = mockk() + val safeSslContextMock = mockk { + every { socketFactory } returns mockk() + } + + val unsafeSslContextMock = mockk { + every { init(any(), any(), any()) } answers {} + every { socketFactory } returns mockk() + } + + val sslContextGetInstanceMock: (String) -> SSLContext = SSLContext::getInstance + mockkStatic(sslContextGetInstanceMock as KFunction<*>) + every { sslContextGetInstanceMock("TLSv1.2") } returns unsafeSslContextMock + + val certManagerMock = mockk { + every { trustManager } returns safeTrustManagerMock + every { sslContext } returns safeSslContextMock + } + + mockkObject(CertificateManager.Companion) + every { CertificateManager.getInstance() } returns certManagerMock + + val okHttpClientBuilderMock = mockk { + every { readTimeout(any(), TimeUnit.MINUTES) } returns this + every { connectTimeout(any(), TimeUnit.MINUTES) } returns this + every { connectionPool(any()) } returns this + every { dispatcher(any()) } returns this + every { addInterceptor(any()) } returns this + every { connectionSpecs(any>()) } returns this + every { hostnameVerifier(any()) } returns this + every { build() } returns mockk() + } + + val buildApiFunResultMockk = mockk() + val buildApiWithBytesConverterFunResultMockk = mockk() + + mockkStatic("org.zowe.kotlinsdk.ApiKt") + every { buildApi(any(), any(), any>()) } returns buildApiFunResultMockk + every { + buildApiWithBytesConverter(any(), any(), any>()) + } answers { + buildApiWithBytesConverterFunResultMockk + } + + beforeEach { + sslFactoryActual = null + trustManagerActual = null + } + + should("check that getApi returns safe OkHttpClient without bytes converter when self-signed certificates are not allowed") { + mockkConstructor(OkHttpClient.Builder::class) + every { + anyConstructed() + .sslSocketFactory(any(), any()) + } answers { + sslFactoryActual = firstArg() + trustManagerActual = secondArg() + okHttpClientBuilderMock + } + + val connectionConfig = mockk { + every { url } returns "test" + every { isAllowSelfSigned } returns false + } + + val zosmfApiImpl = ZosmfApiImpl() + val resutlActual = zosmfApiImpl.getApi(Any::class.java, connectionConfig) + + assertSoftly { buildApiFunResultMockk shouldBe resutlActual } + assertSoftly { sslFactoryActual shouldNotBe null } + assertSoftly { trustManagerActual shouldNotBe null } + assertSoftly { sslFactoryActual shouldBe safeSslContextMock.socketFactory } + assertSoftly { trustManagerActual shouldBe safeTrustManagerMock } + } + should("check that getApi returns already initialized safe OkHttpClient without bytes converter when self-signed certificates are not allowed") { + mockkConstructor(OkHttpClient.Builder::class) + every { + anyConstructed() + .sslSocketFactory(any(), any()) + } answers { + sslFactoryActual = firstArg() + trustManagerActual = secondArg() + okHttpClientBuilderMock + } + + val connectionConfig = mockk { + every { url } returns "test" + every { isAllowSelfSigned } returns false + } + + val zosmfApiImpl = ZosmfApiImpl() + val resutlActual1 = zosmfApiImpl.getApi(Any::class.java, connectionConfig) + val resutlActual2 = zosmfApiImpl.getApi(Any::class.java, connectionConfig) + + assertSoftly { buildApiFunResultMockk shouldBe resutlActual1 } + assertSoftly { buildApiFunResultMockk shouldBe resutlActual2 } + assertSoftly { resutlActual1 shouldBe resutlActual2 } + // Lazy is not initialized the second time + assertSoftly { sslFactoryActual shouldBe null } + assertSoftly { trustManagerActual shouldBe null } + } + should("check that getApi returns safe OkHttpClient with bytes converter when self-signed certificates are not allowed") { + mockkConstructor(OkHttpClient.Builder::class) + every { + anyConstructed() + .sslSocketFactory(any(), any()) + } answers { + sslFactoryActual = firstArg() + trustManagerActual = secondArg() + okHttpClientBuilderMock + } + + val connectionConfig = mockk { + every { url } returns "test" + every { isAllowSelfSigned } returns false + } + + val zosmfApiImpl = ZosmfApiImpl() + val resutlActual = zosmfApiImpl.getApiWithBytesConverter(Any::class.java, connectionConfig) + + assertSoftly { buildApiWithBytesConverterFunResultMockk shouldBe resutlActual } + // Lazy is not initialized the second time + assertSoftly { sslFactoryActual shouldBe null } + assertSoftly { trustManagerActual shouldBe null } + } + should("check that getApi returns unsafe OkHttpClient without bytes converter when self-signed certificates are allowed") { + mockkConstructor(OkHttpClient.Builder::class) + every { + anyConstructed() + .sslSocketFactory(any(), any()) + } answers { + sslFactoryActual = firstArg() + trustManagerActual = secondArg() + okHttpClientBuilderMock + } + + val connectionConfig = mockk { + every { url } returns "test" + every { isAllowSelfSigned } returns true + } + + val zosmfApiImpl = ZosmfApiImpl() + val resutlActual = zosmfApiImpl.getApi(Any::class.java, connectionConfig) + + assertSoftly { buildApiFunResultMockk shouldBe resutlActual } + assertSoftly { sslFactoryActual shouldNotBe null } + assertSoftly { trustManagerActual shouldNotBe null } + assertSoftly { sslFactoryActual shouldBe unsafeSslContextMock.socketFactory } + assertSoftly { trustManagerActual shouldNotBe safeTrustManagerMock } + } + should("check that getApi returns already initialized unsafe OkHttpClient without bytes converter when self-signed certificates are allowed") { + mockkConstructor(OkHttpClient.Builder::class) + every { + anyConstructed() + .sslSocketFactory(any(), any()) + } answers { + sslFactoryActual = firstArg() + trustManagerActual = secondArg() + okHttpClientBuilderMock + } + + val connectionConfig = mockk { + every { url } returns "test" + every { isAllowSelfSigned } returns true + } + + val zosmfApiImpl = ZosmfApiImpl() + val resutlActual1 = zosmfApiImpl.getApi(Any::class.java, connectionConfig) + val resutlActual2 = zosmfApiImpl.getApi(Any::class.java, connectionConfig) + + assertSoftly { buildApiFunResultMockk shouldBe resutlActual1 } + assertSoftly { buildApiFunResultMockk shouldBe resutlActual2 } + assertSoftly { resutlActual1 shouldBe resutlActual2 } + // Lazy is not initialized the second time + assertSoftly { sslFactoryActual shouldBe null } + assertSoftly { trustManagerActual shouldBe null } + } + should("check that getApi returns unsafe OkHttpClient with bytes converter when self-signed certificates are allowed") { + mockkConstructor(OkHttpClient.Builder::class) + every { + anyConstructed() + .sslSocketFactory(any(), any()) + } answers { + sslFactoryActual = firstArg() + trustManagerActual = secondArg() + okHttpClientBuilderMock + } + + val connectionConfig = mockk { + every { url } returns "test" + every { isAllowSelfSigned } returns true + } + + val zosmfApiImpl = ZosmfApiImpl() + val resutlActual = zosmfApiImpl.getApiWithBytesConverter(Any::class.java, connectionConfig) + + assertSoftly { buildApiWithBytesConverterFunResultMockk shouldBe resutlActual } + // Lazy is not initialized the second time + assertSoftly { sslFactoryActual shouldBe null } + assertSoftly { trustManagerActual shouldBe null } + } + } +}) From c823908896fb3fd06c7be56038fd2b1aed82c07c Mon Sep 17 00:00:00 2001 From: Katsiaryna Tsytsenia Date: Tue, 15 Oct 2024 17:34:09 +0200 Subject: [PATCH 07/15] IJMP-1690 Show DDs regardless of the job execution status --- .../dataops/log/AbstractMFLoggerBase.kt | 7 ++ .../formainframe/dataops/log/MFLogger.kt | 5 + .../ui/build/jobs/JobBuildTreeView.kt | 109 +++++++++++++----- 3 files changed, 90 insertions(+), 31 deletions(-) diff --git a/src/main/kotlin/eu/ibagroup/formainframe/dataops/log/AbstractMFLoggerBase.kt b/src/main/kotlin/eu/ibagroup/formainframe/dataops/log/AbstractMFLoggerBase.kt index 69077adfb..545687aba 100644 --- a/src/main/kotlin/eu/ibagroup/formainframe/dataops/log/AbstractMFLoggerBase.kt +++ b/src/main/kotlin/eu/ibagroup/formainframe/dataops/log/AbstractMFLoggerBase.kt @@ -122,6 +122,13 @@ abstract class AbstractMFLoggerBase> { */ fun onLogFinished(finishHandler: () -> Unit) + /** + * Fetch log files + */ + fun fetchLog() + /** * Sets handler for event after requesting next portion of mainframe log. * @param nextLogHandler handler that will be invoked after next request to fetching log from mainframe. diff --git a/src/main/kotlin/eu/ibagroup/formainframe/ui/build/jobs/JobBuildTreeView.kt b/src/main/kotlin/eu/ibagroup/formainframe/ui/build/jobs/JobBuildTreeView.kt index 5e3126f52..44e5c7efc 100644 --- a/src/main/kotlin/eu/ibagroup/formainframe/ui/build/jobs/JobBuildTreeView.kt +++ b/src/main/kotlin/eu/ibagroup/formainframe/ui/build/jobs/JobBuildTreeView.kt @@ -32,11 +32,14 @@ import eu.ibagroup.formainframe.dataops.DataOpsManager import eu.ibagroup.formainframe.dataops.log.JobLogFetcher import eu.ibagroup.formainframe.dataops.log.JobProcessInfo import eu.ibagroup.formainframe.dataops.log.MFLogger +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking import org.zowe.kotlinsdk.SpoolFile import java.awt.BorderLayout import java.util.* import javax.swing.JComponent import javax.swing.tree.DefaultMutableTreeNode +import javax.swing.tree.TreeNode val JOBS_LOG_VIEW = DataKey.create("jobsLogView") const val JOBS_LOG_NOTIFICATION_GROUP_ID = "eu.ibagroup.formainframe.explorer.ExplorerNotificationGroup" @@ -101,20 +104,25 @@ class JobBuildTreeView( treeConsoleView.component.add(it.component, BorderLayout.PAGE_START) } - jobLogger.onNextLog { + jobLogger.startLogging() + + fun onNextLog() { val cachedSpoolLog = jobLogger.logFetcher.getCachedLog() - cachedSpoolLog - .minus(spoolFileToLogMap.keys) - .forEach { + if (cachedSpoolLog.count() != spoolFileToLogMap.count()) { + cachedSpoolLog.minus(spoolFileToLogMap.keys).forEach { treeConsoleView.onEvent(buildId, StartEventImpl(it.key.id, buildId, Date().time, it.key.ddName)) } - cachedSpoolLog - .forEach { + cachedSpoolLog.forEach { val prevLog = spoolFileToLogMap[it.key] ?: "" val logToDisplay = if (it.value.length >= prevLog.length) it.value.substring(prevLog.length) else prevLog treeConsoleView.onEvent(buildId, OutputBuildEventImpl(it.key.id, logToDisplay, true)) spoolFileToLogMap[it.key] = it.value } + } + } + + jobLogger.onNextLog { + onNextLog() } jobLogger.onLogFinished { @@ -123,43 +131,82 @@ class JobBuildTreeView( .getCachedJobStatus() ?.returnedCode ?.uppercase() - var codeWithWarning = false - val result = if (rc == null || rc.contains(Regex("ERR|ABEND|CANCEL|FAIL"))) FailureResultImpl() - else if (rc.contains("CC")) { // result code can be in format "CC nnnn" + //Variables were added to set the correct icon depending on the result of the job execution. + //For any execution status, FailureResultImpl will be used to display DDs + val ret = if (rc?.contains("CC") == true) { // result code can be in format "CC nnnn" val completionCode = rc.split(" ")[1].toInt() when (completionCode) { - SUCCESSFUL_JOB_COMPLETION_CODE -> SuccessResultImpl() + SUCCESSFUL_JOB_COMPLETION_CODE -> ReturnCode.SUCCESS + SUCCESSFUL_JOB_COMPLETION_CODE_WITH_WARNING -> ReturnCode.WARNING + else -> ReturnCode.ERROR + } + } else ReturnCode.ERROR - SUCCESSFUL_JOB_COMPLETION_CODE_WITH_WARNING -> { - codeWithWarning = true - SuccessResultImpl() - } + jobLogger.fetchLog() + var finalLogFiles = jobLogger.logFetcher.getCachedLog() + if (finalLogFiles.count() != spoolFileToLogMap.count()) { + onNextLog() + } - else -> FailureResultImpl() - } - } else SuccessResultImpl() + runBlocking { + //TODO Need to be reworked + //It is possible that not all DDs are displayed (but information about them already exists), + // And, accordingly, the correct icon cannot be set for them. + //Sleep for 1 second to wait for display + if ((treeConsoleView.tree.model.root as DefaultMutableTreeNode).firstChild.childCount < spoolFileToLogMap.count()) + delay(1000) + + setIconRec( + (treeConsoleView.tree.model.root as DefaultMutableTreeNode).firstChild, + ret + ) + } - jobLogger.logFetcher.getCachedLog() + finalLogFiles .forEach { - treeConsoleView.onEvent(buildId, FinishEventImpl(it.key.id, buildId, Date().time, it.key.ddName, result)) - } - runCatching { - val buildNode = (treeConsoleView.tree.model.root as DefaultMutableTreeNode).firstChild - val buildExecutionNode = (buildNode as DefaultMutableTreeNode).userObject as ExecutionNode - if (result is FailureResultImpl) { - buildExecutionNode.setIconProvider { AllIcons.General.BalloonError } - } else if (codeWithWarning) { - buildExecutionNode.setIconProvider { AllIcons.General.BalloonWarning } - } else { - buildExecutionNode.setIconProvider { AllIcons.General.InspectionsOK } + treeConsoleView.onEvent( + buildId, + FinishEventImpl(it.key.id, buildId, Date().time, it.key.ddName, FailureResultImpl()) + ) } + treeConsoleView.onEvent( + buildId, + FinishBuildEventImpl(buildId, buildId, Date().time, buildId, FailureResultImpl()) + ) + } + } + + /** + * The function recursively sets the job execution status icon + */ + private fun setIconRec(buildNode: TreeNode, ret: ReturnCode) { + setIcon(buildNode, ret) + if (buildNode.childCount > 0) + for (currChild in buildNode.children()) { + setIcon(currChild, ret) } - treeConsoleView.onEvent(buildId, FinishBuildEventImpl(buildId, buildId, Date().time, buildId, result)) + } + + /** + * The function sets the job execution status icon + */ + private fun setIcon(buildNode: TreeNode, ret: ReturnCode) { + val buildExecutionNode = (buildNode as DefaultMutableTreeNode).userObject as ExecutionNode + when (ret) { + ReturnCode.ERROR -> buildExecutionNode.setIconProvider { AllIcons.General.BalloonError } + ReturnCode.WARNING -> buildExecutionNode.setIconProvider { AllIcons.General.BalloonWarning } + ReturnCode.SUCCESS -> buildExecutionNode.setIconProvider { AllIcons.General.InspectionsOK } } + } - jobLogger.startLogging() + /** + * Enum for job execution status + */ + enum class ReturnCode { + SUCCESS, WARNING, ERROR } + /** * Stops requesting logs from mainframe. */ From fe3ead74a78174f0863463cd7931bd79be77f237 Mon Sep 17 00:00:00 2001 From: Katsiaryna Tsytsenia Date: Tue, 15 Oct 2024 17:34:37 +0200 Subject: [PATCH 08/15] IJMP-1945 Show DDs regardless of the job execution status --- .../dataops/log/AbstractMFLoggerBase.kt | 7 ++ .../formainframe/dataops/log/MFLogger.kt | 5 + .../ui/build/jobs/JobBuildTreeView.kt | 109 +++++++++++++----- 3 files changed, 90 insertions(+), 31 deletions(-) diff --git a/src/main/kotlin/eu/ibagroup/formainframe/dataops/log/AbstractMFLoggerBase.kt b/src/main/kotlin/eu/ibagroup/formainframe/dataops/log/AbstractMFLoggerBase.kt index 69077adfb..545687aba 100644 --- a/src/main/kotlin/eu/ibagroup/formainframe/dataops/log/AbstractMFLoggerBase.kt +++ b/src/main/kotlin/eu/ibagroup/formainframe/dataops/log/AbstractMFLoggerBase.kt @@ -122,6 +122,13 @@ abstract class AbstractMFLoggerBase> { */ fun onLogFinished(finishHandler: () -> Unit) + /** + * Fetch log files + */ + fun fetchLog() + /** * Sets handler for event after requesting next portion of mainframe log. * @param nextLogHandler handler that will be invoked after next request to fetching log from mainframe. diff --git a/src/main/kotlin/eu/ibagroup/formainframe/ui/build/jobs/JobBuildTreeView.kt b/src/main/kotlin/eu/ibagroup/formainframe/ui/build/jobs/JobBuildTreeView.kt index 5e3126f52..44e5c7efc 100644 --- a/src/main/kotlin/eu/ibagroup/formainframe/ui/build/jobs/JobBuildTreeView.kt +++ b/src/main/kotlin/eu/ibagroup/formainframe/ui/build/jobs/JobBuildTreeView.kt @@ -32,11 +32,14 @@ import eu.ibagroup.formainframe.dataops.DataOpsManager import eu.ibagroup.formainframe.dataops.log.JobLogFetcher import eu.ibagroup.formainframe.dataops.log.JobProcessInfo import eu.ibagroup.formainframe.dataops.log.MFLogger +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking import org.zowe.kotlinsdk.SpoolFile import java.awt.BorderLayout import java.util.* import javax.swing.JComponent import javax.swing.tree.DefaultMutableTreeNode +import javax.swing.tree.TreeNode val JOBS_LOG_VIEW = DataKey.create("jobsLogView") const val JOBS_LOG_NOTIFICATION_GROUP_ID = "eu.ibagroup.formainframe.explorer.ExplorerNotificationGroup" @@ -101,20 +104,25 @@ class JobBuildTreeView( treeConsoleView.component.add(it.component, BorderLayout.PAGE_START) } - jobLogger.onNextLog { + jobLogger.startLogging() + + fun onNextLog() { val cachedSpoolLog = jobLogger.logFetcher.getCachedLog() - cachedSpoolLog - .minus(spoolFileToLogMap.keys) - .forEach { + if (cachedSpoolLog.count() != spoolFileToLogMap.count()) { + cachedSpoolLog.minus(spoolFileToLogMap.keys).forEach { treeConsoleView.onEvent(buildId, StartEventImpl(it.key.id, buildId, Date().time, it.key.ddName)) } - cachedSpoolLog - .forEach { + cachedSpoolLog.forEach { val prevLog = spoolFileToLogMap[it.key] ?: "" val logToDisplay = if (it.value.length >= prevLog.length) it.value.substring(prevLog.length) else prevLog treeConsoleView.onEvent(buildId, OutputBuildEventImpl(it.key.id, logToDisplay, true)) spoolFileToLogMap[it.key] = it.value } + } + } + + jobLogger.onNextLog { + onNextLog() } jobLogger.onLogFinished { @@ -123,43 +131,82 @@ class JobBuildTreeView( .getCachedJobStatus() ?.returnedCode ?.uppercase() - var codeWithWarning = false - val result = if (rc == null || rc.contains(Regex("ERR|ABEND|CANCEL|FAIL"))) FailureResultImpl() - else if (rc.contains("CC")) { // result code can be in format "CC nnnn" + //Variables were added to set the correct icon depending on the result of the job execution. + //For any execution status, FailureResultImpl will be used to display DDs + val ret = if (rc?.contains("CC") == true) { // result code can be in format "CC nnnn" val completionCode = rc.split(" ")[1].toInt() when (completionCode) { - SUCCESSFUL_JOB_COMPLETION_CODE -> SuccessResultImpl() + SUCCESSFUL_JOB_COMPLETION_CODE -> ReturnCode.SUCCESS + SUCCESSFUL_JOB_COMPLETION_CODE_WITH_WARNING -> ReturnCode.WARNING + else -> ReturnCode.ERROR + } + } else ReturnCode.ERROR - SUCCESSFUL_JOB_COMPLETION_CODE_WITH_WARNING -> { - codeWithWarning = true - SuccessResultImpl() - } + jobLogger.fetchLog() + var finalLogFiles = jobLogger.logFetcher.getCachedLog() + if (finalLogFiles.count() != spoolFileToLogMap.count()) { + onNextLog() + } - else -> FailureResultImpl() - } - } else SuccessResultImpl() + runBlocking { + //TODO Need to be reworked + //It is possible that not all DDs are displayed (but information about them already exists), + // And, accordingly, the correct icon cannot be set for them. + //Sleep for 1 second to wait for display + if ((treeConsoleView.tree.model.root as DefaultMutableTreeNode).firstChild.childCount < spoolFileToLogMap.count()) + delay(1000) + + setIconRec( + (treeConsoleView.tree.model.root as DefaultMutableTreeNode).firstChild, + ret + ) + } - jobLogger.logFetcher.getCachedLog() + finalLogFiles .forEach { - treeConsoleView.onEvent(buildId, FinishEventImpl(it.key.id, buildId, Date().time, it.key.ddName, result)) - } - runCatching { - val buildNode = (treeConsoleView.tree.model.root as DefaultMutableTreeNode).firstChild - val buildExecutionNode = (buildNode as DefaultMutableTreeNode).userObject as ExecutionNode - if (result is FailureResultImpl) { - buildExecutionNode.setIconProvider { AllIcons.General.BalloonError } - } else if (codeWithWarning) { - buildExecutionNode.setIconProvider { AllIcons.General.BalloonWarning } - } else { - buildExecutionNode.setIconProvider { AllIcons.General.InspectionsOK } + treeConsoleView.onEvent( + buildId, + FinishEventImpl(it.key.id, buildId, Date().time, it.key.ddName, FailureResultImpl()) + ) } + treeConsoleView.onEvent( + buildId, + FinishBuildEventImpl(buildId, buildId, Date().time, buildId, FailureResultImpl()) + ) + } + } + + /** + * The function recursively sets the job execution status icon + */ + private fun setIconRec(buildNode: TreeNode, ret: ReturnCode) { + setIcon(buildNode, ret) + if (buildNode.childCount > 0) + for (currChild in buildNode.children()) { + setIcon(currChild, ret) } - treeConsoleView.onEvent(buildId, FinishBuildEventImpl(buildId, buildId, Date().time, buildId, result)) + } + + /** + * The function sets the job execution status icon + */ + private fun setIcon(buildNode: TreeNode, ret: ReturnCode) { + val buildExecutionNode = (buildNode as DefaultMutableTreeNode).userObject as ExecutionNode + when (ret) { + ReturnCode.ERROR -> buildExecutionNode.setIconProvider { AllIcons.General.BalloonError } + ReturnCode.WARNING -> buildExecutionNode.setIconProvider { AllIcons.General.BalloonWarning } + ReturnCode.SUCCESS -> buildExecutionNode.setIconProvider { AllIcons.General.InspectionsOK } } + } - jobLogger.startLogging() + /** + * Enum for job execution status + */ + enum class ReturnCode { + SUCCESS, WARNING, ERROR } + /** * Stops requesting logs from mainframe. */ From 7d942f679fabb6a4a0fef71d8fa42ece1421f7cd Mon Sep 17 00:00:00 2001 From: Arseni Tsikhamirau Date: Thu, 24 Oct 2024 10:29:48 +0200 Subject: [PATCH 09/15] IJMP-1962-Automatic-refresh-is-not-performed-after-purge-on-filter-with-JOBID --- .../formainframe/explorer/actions/PurgeJobAction.kt | 13 ++++++++----- .../explorer/actions/PurgeJobActionTestSpec.kt | 8 ++++---- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/main/kotlin/eu/ibagroup/formainframe/explorer/actions/PurgeJobAction.kt b/src/main/kotlin/eu/ibagroup/formainframe/explorer/actions/PurgeJobAction.kt index 9846cd5d9..ea9f0b5f3 100644 --- a/src/main/kotlin/eu/ibagroup/formainframe/explorer/actions/PurgeJobAction.kt +++ b/src/main/kotlin/eu/ibagroup/formainframe/explorer/actions/PurgeJobAction.kt @@ -118,11 +118,14 @@ class PurgeJobAction : AnAction() { jobsByFilterWaitingPurgeMap.keys.forEach { filterNode -> val query = filterNode.query if (query != null) { + // If we performed purge from the job filter defined with JOBID, then OWNER and PREFIX should be "*". + // Do not send userCorrelator for a job filter defined with JOBID as described in the specification val response = api(query.connectionConfig).getFilteredJobs( basicCredentials = query.connectionConfig.authToken, - owner = query.request.owner, - prefix = query.request.prefix, - userCorrelator = query.request.userCorrelatorFilter, + owner = query.request.owner.ifEmpty { "*" }, + prefix = query.request.prefix.ifEmpty { "*" }, + jobId = query.request.jobId.ifEmpty { null }, + userCorrelator = query.request.userCorrelatorFilter.ifEmpty { null }, execData = ExecData.YES ).execute() val result = response.body() @@ -153,9 +156,9 @@ class PurgeJobAction : AnAction() { if (filtersWithRefreshErrors.isNotEmpty()) { var errorFilterNames = "" for (entry in filtersWithRefreshErrors) { - errorFilterNames += entry.key.unit.name + "\n" + errorFilterNames += entry.key.name + "\n" } - throw RuntimeException("Refresh error. Failed filters are: $errorFilterNames") + throw RuntimeException("Refresh error. Failed job filters are: $errorFilterNames") } } diff --git a/src/test/kotlin/eu/ibagroup/formainframe/explorer/actions/PurgeJobActionTestSpec.kt b/src/test/kotlin/eu/ibagroup/formainframe/explorer/actions/PurgeJobActionTestSpec.kt index 94c580d18..67ef78409 100644 --- a/src/test/kotlin/eu/ibagroup/formainframe/explorer/actions/PurgeJobActionTestSpec.kt +++ b/src/test/kotlin/eu/ibagroup/formainframe/explorer/actions/PurgeJobActionTestSpec.kt @@ -321,6 +321,7 @@ class PurgeJobActionTestSpec : WithApplicationShouldSpec({ every { jobParent1.query?.request } returns mockk() every { jobParent1.query?.request?.prefix } returns "prefix_1" every { jobParent1.query?.request?.owner } returns "owner" + every { jobParent1.query?.request?.jobId } returns "" every { jobParent1.query?.request?.userCorrelatorFilter } returns "filter_1" every { jobParentParent1.unit } returns mockk() @@ -334,6 +335,7 @@ class PurgeJobActionTestSpec : WithApplicationShouldSpec({ every { jobParent2.query?.request } returns mockk() every { jobParent2.query?.request?.prefix } returns "prefix_2" every { jobParent2.query?.request?.owner } returns "owner" + every { jobParent2.query?.request?.jobId } returns "" every { jobParent2.query?.request?.userCorrelatorFilter } returns "filter_2" every { jobParentParent2.unit } returns mockk() every { jobParentParent2.unit.name } returns "secondWS" @@ -519,10 +521,8 @@ class PurgeJobActionTestSpec : WithApplicationShouldSpec({ should("purge actionPerformed when jobs have been purged successfully, but refresh by filter fails") { every { responseMockk.isSuccessful } returns false every { responseMockk.body() } returns listOf() - every { jobParent1.unit } returns mockk() - every { jobParent1.unit.name } returns "firstFilter" - every { jobParent2.unit } returns mockk() - every { jobParent2.unit.name } returns "secondFilter" + every { jobParent1.name } returns "firstFilter" + every { jobParent2.name } returns "secondFilter" assertThrows(RuntimeException::class.java) { purgeAction.actionPerformed(mockActionEventForJesEx) From e625437027e42d12e31fc8e85a1b7f3b0e25a72e Mon Sep 17 00:00:00 2001 From: Uladzislau Lailo Date: Fri, 25 Oct 2024 13:47:47 +0200 Subject: [PATCH 10/15] IJMP-1947 connection url fix --- .../connect/ui/zosmf/ConnectionDialog.kt | 29 ++++++--- .../formainframe/utils/validationFunctions.kt | 3 +- .../formainframe/utils/UtilsTestSpec.kt | 60 ++++++++++++++++++- 3 files changed, 80 insertions(+), 12 deletions(-) diff --git a/src/main/kotlin/eu/ibagroup/formainframe/config/connect/ui/zosmf/ConnectionDialog.kt b/src/main/kotlin/eu/ibagroup/formainframe/config/connect/ui/zosmf/ConnectionDialog.kt index 2859e70f4..a2657279a 100644 --- a/src/main/kotlin/eu/ibagroup/formainframe/config/connect/ui/zosmf/ConnectionDialog.kt +++ b/src/main/kotlin/eu/ibagroup/formainframe/config/connect/ui/zosmf/ConnectionDialog.kt @@ -31,12 +31,17 @@ import eu.ibagroup.formainframe.config.connect.* import eu.ibagroup.formainframe.config.connect.ui.ChangePasswordDialog import eu.ibagroup.formainframe.config.connect.ui.ChangePasswordDialogState import eu.ibagroup.formainframe.dataops.DataOpsManager -import eu.ibagroup.formainframe.dataops.operations.* +import eu.ibagroup.formainframe.dataops.operations.ChangePasswordOperation +import eu.ibagroup.formainframe.dataops.operations.InfoOperation +import eu.ibagroup.formainframe.dataops.operations.ZOSInfoOperation import eu.ibagroup.formainframe.explorer.EXPLORER_NOTIFICATION_GROUP_ID -import eu.ibagroup.formainframe.utils.* import eu.ibagroup.formainframe.utils.crudable.Crudable import eu.ibagroup.formainframe.utils.crudable.find import eu.ibagroup.formainframe.utils.crudable.getAll +import eu.ibagroup.formainframe.utils.runTask +import eu.ibagroup.formainframe.utils.validateConnectionName +import eu.ibagroup.formainframe.utils.validateForBlank +import eu.ibagroup.formainframe.utils.validateZosmfUrl import org.zowe.kotlinsdk.ChangePassword import org.zowe.kotlinsdk.annotations.ZVersion import java.awt.Component @@ -59,10 +64,11 @@ class ConnectionDialog( * 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 { /** Show Test connection dialog and test the connection regarding the dialog state. @@ -81,11 +87,17 @@ class ConnectionDialog( initialState = initialState, factory = { ConnectionDialog(crudable, initialState, project) }, test = { state -> - val newTestedConnConfig : ConnectionConfig + val newTestedConnConfig: ConnectionConfig if (initialState.mode == DialogMode.UPDATE) { val newState = state.clone() newState.initEmptyUuids(crudable) - newTestedConnConfig = ConnectionConfig(newState.connectionUuid, newState.connectionName, newState.connectionUrl, newState.isAllowSsl, newState.zVersion) + newTestedConnConfig = ConnectionConfig( + newState.connectionUuid, + newState.connectionName, + newState.connectionUrl, + newState.isAllowSsl, + newState.zVersion + ) CredentialService.instance.setCredentials( connectionConfigUuid = newState.connectionUuid, username = newState.username, @@ -97,7 +109,8 @@ class ConnectionDialog( CredentialService.instance.setCredentials( connectionConfigUuid = state.connectionUuid, username = state.username, - password = state.password) + password = state.password + ) } val throwable = runTask(title = "Testing Connection to ${newTestedConnConfig.url}", project = project) { return@runTask try { @@ -169,7 +182,7 @@ class ConnectionDialog( EXPLORER_NOTIFICATION_GROUP_ID, "Unable to retrieve USS username", "Cannot retrieve USS username. An error happened while executing TSO request.\n" + - "When working with USS files the same username will be used that was specified by the user when connecting.", + "When working with USS files the same username will be used that was specified by the user when connecting.", NotificationType.WARNING ).let { Notifications.Bus.notify(it, project) @@ -215,7 +228,7 @@ class ConnectionDialog( textField() .bindText(state::connectionUrl) .validationOnApply { - it.text = it.text.trim().removeTrailingSlashes() + it.text = it.text.trim() validateForBlank(it) ?: validateZosmfUrl(it) } .also { urlTextField = it.component } diff --git a/src/main/kotlin/eu/ibagroup/formainframe/utils/validationFunctions.kt b/src/main/kotlin/eu/ibagroup/formainframe/utils/validationFunctions.kt index 1f24d410c..9857e0f58 100644 --- a/src/main/kotlin/eu/ibagroup/formainframe/utils/validationFunctions.kt +++ b/src/main/kotlin/eu/ibagroup/formainframe/utils/validationFunctions.kt @@ -21,12 +21,11 @@ import eu.ibagroup.formainframe.explorer.ui.UssDirNode import eu.ibagroup.formainframe.explorer.ui.UssFileNode import eu.ibagroup.formainframe.utils.crudable.Crudable import eu.ibagroup.formainframe.utils.crudable.find -import org.zowe.kotlinsdk.DatasetOrganization import javax.swing.JComponent import javax.swing.JPasswordField import javax.swing.JTextField -private val urlRegex = Regex("^(https?|http)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]") +private val urlRegex = Regex("^https?:\\/\\/((([a-zA-Z0-9\\-_]+\\.)+[a-zA-Z]{2,})|(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}))(:\\d+)?((\\/[a-zA-Z0-9\\-_\\.~]+)?)*\\/?\$") private val maskRegex = Regex("^[A-Za-z\\$\\*%@#][A-Za-z0-9\\-\\$\\*%@#]{0,7}") private val ussPathRegex = Regex("^/$|^(/[^/]+)+$") private val forbiddenSymbol = "/" diff --git a/src/test/kotlin/eu/ibagroup/formainframe/utils/UtilsTestSpec.kt b/src/test/kotlin/eu/ibagroup/formainframe/utils/UtilsTestSpec.kt index c964ae803..3d75e0f16 100644 --- a/src/test/kotlin/eu/ibagroup/formainframe/utils/UtilsTestSpec.kt +++ b/src/test/kotlin/eu/ibagroup/formainframe/utils/UtilsTestSpec.kt @@ -427,22 +427,78 @@ class UtilsTestSpec : ShouldSpec({ should("validate correct URL") { component.text = "https://some.url" - val actual = validateZosmfUrl(component) + var actual = validateZosmfUrl(component) val expected = null assertSoftly { actual shouldBe expected } + + component.text = "http://another.url" + actual = validateZosmfUrl(component) + + assertSoftly { + actual shouldBe expected + } + + component.text = "http://10.10.10.10" + actual = validateZosmfUrl(component) + + assertSoftly { + actual shouldBe expected + } + + component.text = "https://10.10.10.10:10000" + actual = validateZosmfUrl(component) + + assertSoftly { + actual shouldBe expected + } + + component.text = "http://another.url/hello/world" + actual = validateZosmfUrl(component) + + assertSoftly { + actual shouldBe expected + } } should("validate wrong URL") { component.text = "wrong url\"" - val actual = validateZosmfUrl(component) + var actual = validateZosmfUrl(component) val expected = ValidationInfo("Please provide a valid URL to z/OSMF. Example: https://myhost.com:10443", component) assertSoftly { actual shouldBe expected } + + component.text = "another.url" + actual = validateZosmfUrl(component) + + assertSoftly { + actual shouldBe expected + } + + component.text = "http://another" + actual = validateZosmfUrl(component) + + assertSoftly { + actual shouldBe expected + } + + component.text = "http://10.10.10:10" + actual = validateZosmfUrl(component) + + assertSoftly { + actual shouldBe expected + } + + component.text = "http://another.url/hell?0/world&!?" + actual = validateZosmfUrl(component) + + assertSoftly { + actual shouldBe expected + } } } context("validateFieldWithLengthRestriction") { From 3bd90e11bdafa3aadd148ad16b6b478bb915e0ca Mon Sep 17 00:00:00 2001 From: Arseni Tsikhamirau Date: Mon, 28 Oct 2024 18:37:02 +0100 Subject: [PATCH 11/15] IJMP-2020-Error-when-uploading-local-file-to-PDS --- .../CrossSystemMemberOrUssFileToPdsMover.kt | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/eu/ibagroup/formainframe/dataops/operations/mover/CrossSystemMemberOrUssFileToPdsMover.kt b/src/main/kotlin/eu/ibagroup/formainframe/dataops/operations/mover/CrossSystemMemberOrUssFileToPdsMover.kt index f3823afff..c5c1b838e 100644 --- a/src/main/kotlin/eu/ibagroup/formainframe/dataops/operations/mover/CrossSystemMemberOrUssFileToPdsMover.kt +++ b/src/main/kotlin/eu/ibagroup/formainframe/dataops/operations/mover/CrossSystemMemberOrUssFileToPdsMover.kt @@ -42,6 +42,8 @@ class CrossSystemMemberOrUssFileToPdsMoverFactory : OperationRunnerFactory { */ class CrossSystemMemberOrUssFileToPdsMover(val dataOpsManager: DataOpsManager) : AbstractFileMover() { + private val sourceContentType = "text/plain; charset=UTF-8" + /** * Checks that source is member or uss file, dest is partitioned data set, and they are located inside different systems. * @see OperationRunner.canRun @@ -89,17 +91,22 @@ class CrossSystemMemberOrUssFileToPdsMover(val dataOpsManager: DataOpsManager) : else XIBMDataType(XIBMDataType.Type.TEXT) val sourceContent = sourceFile.contentsToByteArray() - val contentToUpload = - if (sourceFile.fileType.isBinary) sourceContent - else sourceContent.toString(sourceFile.charset).replace("\r\n", "\n") - .toByteArray(DEFAULT_TEXT_CHARSET).addNewLine() + + // do not convert bytes to default ISO. z/OSMF does this conversion by calling iconv and produce the desired result + val contentToUpload = if (sourceFile.fileType.isBinary) sourceContent else + sourceContent + .toString(sourceFile.charset) + .replace("\r\n", "\n") + .toByteArray() + .addNewLine() val response = apiWithBytesConverter(destConnectionConfig).writeToDatasetMember( authorizationToken = destConnectionConfig.authToken, datasetName = destAttributes.name, memberName = memberName, content = contentToUpload, - xIBMDataType = xIBMDataType + xIBMDataType = xIBMDataType, + contentType = sourceContentType ).applyIfNotNull(progressIndicator) { indicator -> cancelByIndicator(indicator) }.execute() From 5c244d76c298872ebfd0bfaf362eb7a77504f566 Mon Sep 17 00:00:00 2001 From: Uladzislau Lailo Date: Wed, 23 Oct 2024 17:09:25 +0300 Subject: [PATCH 12/15] IJMP-1862 cross-system folder coping --- .../dataops/operations/mover/CrossSystemUssDirMover.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/eu/ibagroup/formainframe/dataops/operations/mover/CrossSystemUssDirMover.kt b/src/main/kotlin/eu/ibagroup/formainframe/dataops/operations/mover/CrossSystemUssDirMover.kt index e7cf620f0..02ebec181 100644 --- a/src/main/kotlin/eu/ibagroup/formainframe/dataops/operations/mover/CrossSystemUssDirMover.kt +++ b/src/main/kotlin/eu/ibagroup/formainframe/dataops/operations/mover/CrossSystemUssDirMover.kt @@ -86,7 +86,7 @@ class CrossSystemUssDirMover(val dataOpsManager: DataOpsManager) : AbstractFileM sourceFileFetchProvider.reload(sourceQuery) } - val pathToDir = destAttributes.path + "/" + sourceFile.name + val pathToDir = destAttributes.path + "/" + (operation.newName ?: sourceFile.name) if (operation.forceOverwriting) { destFile.children.firstOrNull { it.name == sourceFile.name }?.let { @@ -110,7 +110,7 @@ class CrossSystemUssDirMover(val dataOpsManager: DataOpsManager) : AbstractFileM val createdDirFile = attributesService.getOrCreateVirtualFile( RemoteUssAttributes( destAttributes.path, - UssFile(sourceFile.name, "drwxrwxrwx"), + UssFile(operation.newName ?: sourceFile.name, "drwxrwxrwx"), destConnectionConfig.url, destConnectionConfig ) @@ -153,4 +153,4 @@ class CrossSystemUssDirMover(val dataOpsManager: DataOpsManager) : AbstractFileM } log.info("USS directory has been moved successfully") } -} \ No newline at end of file +} From 9a18867f5bd034896019f4fe6fc1c67864857585 Mon Sep 17 00:00:00 2001 From: Arseni Tsikhamirau Date: Tue, 29 Oct 2024 10:19:17 +0100 Subject: [PATCH 13/15] IJMP-1992-Uss-children-are-not-refreshed-if-refresh-on-parent --- .../dataops/fetch/RemoteFileFetchProviderBase.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/eu/ibagroup/formainframe/dataops/fetch/RemoteFileFetchProviderBase.kt b/src/main/kotlin/eu/ibagroup/formainframe/dataops/fetch/RemoteFileFetchProviderBase.kt index b04428867..81b075cf1 100644 --- a/src/main/kotlin/eu/ibagroup/formainframe/dataops/fetch/RemoteFileFetchProviderBase.kt +++ b/src/main/kotlin/eu/ibagroup/formainframe/dataops/fetch/RemoteFileFetchProviderBase.kt @@ -15,7 +15,6 @@ package eu.ibagroup.formainframe.dataops.fetch import com.intellij.ide.util.treeView.AbstractTreeNode -import com.intellij.openapi.components.service import com.intellij.openapi.progress.ProcessCanceledException import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.vfs.VirtualFile @@ -24,6 +23,7 @@ import eu.ibagroup.formainframe.config.connect.ConnectionConfigBase import eu.ibagroup.formainframe.dataops.DataOpsManager import eu.ibagroup.formainframe.dataops.Query import eu.ibagroup.formainframe.dataops.RemoteQuery +import eu.ibagroup.formainframe.dataops.UnitRemoteQueryImpl import eu.ibagroup.formainframe.dataops.exceptions.CallException import eu.ibagroup.formainframe.dataops.services.ErrorSeparatorService import eu.ibagroup.formainframe.utils.castOrNull @@ -223,7 +223,8 @@ abstract class RemoteFileFetchProviderBase Date: Tue, 29 Oct 2024 13:06:17 +0100 Subject: [PATCH 14/15] IJMP-1538: add file tag mapping with CCSID value --- .../formainframe/utils/ussFileTagUtils.kt | 63 +++++++++++++++---- .../resources/messages/FMBundle.properties | 6 +- 2 files changed, 56 insertions(+), 13 deletions(-) diff --git a/src/main/kotlin/eu/ibagroup/formainframe/utils/ussFileTagUtils.kt b/src/main/kotlin/eu/ibagroup/formainframe/utils/ussFileTagUtils.kt index ac2f9b523..2497bc47c 100644 --- a/src/main/kotlin/eu/ibagroup/formainframe/utils/ussFileTagUtils.kt +++ b/src/main/kotlin/eu/ibagroup/formainframe/utils/ussFileTagUtils.kt @@ -15,6 +15,7 @@ import com.intellij.notification.Notification import com.intellij.notification.NotificationType import com.intellij.notification.Notifications import com.intellij.openapi.components.service +import eu.ibagroup.formainframe.common.message import eu.ibagroup.formainframe.config.connect.ConnectionConfig import eu.ibagroup.formainframe.dataops.DataOpsManager import eu.ibagroup.formainframe.dataops.attributes.RemoteUssAttributes @@ -37,14 +38,41 @@ const val FILE_TAG_NOTIFICATION_GROUP_ID = "eu.ibagroup.formainframe.utils.FileT fun checkUssFileTag(attributes: RemoteUssAttributes) { val charset = getUssFileTagCharset(attributes) if (charset != null) { - attributes.charset = charset + if (getSupportedEncodings().contains(charset)) { + attributes.charset = charset + } else { + notifyError( + message("filetag.unsupported.encoding.error.title", charset), + message("filetag.unsupported.encoding.error.message", DEFAULT_BINARY_CHARSET.name()) + ) + attributes.charset = DEFAULT_BINARY_CHARSET + } } else { attributes.charset = DEFAULT_BINARY_CHARSET } } +/** Matching file tag with CCSID value */ +private val fileTagToCcsidMap = mapOf( + "TIS-620" to 874, + "ISO8859-13" to 921, + "IBM-EUCJC" to 932, + "IBM-943" to 943, + "BIG5" to 950, + "IBM-4396" to 4396, + "IBM-4946" to 4946, + "IBM-5031" to 5031, + "IBM-5346" to 5346, + "IBM-5347" to 5347, + "IBM-5348" to 5348, + "IBM-5349" to 5349, + "IBM-5350" to 5350, + "IBM-5488" to 5488, + "EUCJP" to 33722, +) + /** - * Get encoding from file tag or return null if it doesn't exist. + * Get encoding from file tag or return null if it doesn't exist or encoding could not be determined. * @param attributes uss file attributes. */ fun getUssFileTagCharset(attributes: RemoteUssAttributes): Charset? { @@ -56,9 +84,20 @@ fun getUssFileTagCharset(attributes: RemoteUssAttributes): Charset? { val startPos = stdout.indexOfNonWhitespace(1) val endPos = stdout.indexOf(' ', startPos) val tagCharset = stdout.substring(startPos, endPos) - val ccsid = CCSID.getCCSID(tagCharset) - val codePage = CCSID.getCodepage(ccsid) - return Charset.forName(codePage) + runCatching { + val ccsid = fileTagToCcsidMap[tagCharset] ?: CCSID.getCCSID(tagCharset) + val codePage = CCSID.getCodepage(ccsid) + return Charset.forName(codePage) + }.onFailure { + runCatching { + return Charset.forName(tagCharset) + }.onFailure { + notifyError( + message("filetag.encoding.detection.error.title"), + message("filetag.encoding.detection.error.message", DEFAULT_BINARY_CHARSET.name()), + ) + } + } } } return null @@ -96,7 +135,7 @@ fun listUssFileTag(attributes: RemoteUssAttributes): ResponseBody? { ), ) }.onFailure { - notifyError(it,"Cannot list uss file tag for ${attributes.path}") + notifyError("Cannot list uss file tag for ${attributes.path}", it.message ?: "") } return response } @@ -146,7 +185,7 @@ private fun setUssFileTagCommon(charsetName: String, filePath: String, connectio ) ) }.onFailure { - notifyError(it, "Cannot set uss file tag for $filePath") + notifyError("Cannot set uss file tag for $filePath", it.message ?: "") } } @@ -167,21 +206,21 @@ fun removeUssFileTag(attributes: RemoteUssAttributes) { ) ) }.onFailure { - notifyError(it, "Cannot remove uss file tag for ${attributes.path}") + notifyError("Cannot remove uss file tag for ${attributes.path}", it.message ?: "") } } /** * Displays an error notification if an error was received. - * @param th thrown error. - * @param title error text. + * @param title error title. + * @param message error message. */ -private fun notifyError(th: Throwable, title: String) { +private fun notifyError(title: String, message: String) { Notifications.Bus.notify( Notification( FILE_TAG_NOTIFICATION_GROUP_ID, title, - th.message ?: "", + message, NotificationType.ERROR ) ) diff --git a/src/main/resources/messages/FMBundle.properties b/src/main/resources/messages/FMBundle.properties index 85709ae3e..ad6cc7c7b 100755 --- a/src/main/resources/messages/FMBundle.properties +++ b/src/main/resources/messages/FMBundle.properties @@ -27,4 +27,8 @@ encoding.reload.dialog.title={0}: Reload to {1} encoding.reload.dialog.message=The encoding you'''ve chosen ('{1}') may change the contents of '{0}'.
Do you want to Reload the file from remote in the new encoding '{1}' and overwrite contents (may not display correctly).
encoding.convert.button.error.tooltip=Encoding conversion is not available because more than one project is open allocation.dialog.unit.size.hint.description=For IBM 3390 direct access storage device:
1 CYLINDER = 15 TRACKS
1 TRACK = 56664 BYTES -allocation.dialog.unit.size.hint.title=Allocation unit Size \ No newline at end of file +allocation.dialog.unit.size.hint.title=Allocation unit Size +filetag.encoding.detection.error.title=File Encoding Detection Error +filetag.encoding.detection.error.message=Failed to determine the file encoding. The default encoding {0} will be set. It can be changed through the file properties +filetag.unsupported.encoding.error.title=Unsupported Encoding {0} +filetag.unsupported.encoding.error.message=The file encoding is unsupported. The default encoding {0} will be set. It can be changed through the file properties \ No newline at end of file From 5f3629ba72a870ce834d04be1e45e73209d73fcf Mon Sep 17 00:00:00 2001 From: Uladzislau Date: Tue, 29 Oct 2024 15:23:13 +0100 Subject: [PATCH 15/15] Small fixes Signed-off-by: Uladzislau --- .../connect/ui/zosmf/ConnectionDialog.kt | 5 +---- .../ui/build/jobs/JobBuildTreeView.kt | 5 ++++- .../formainframe/utils/ussFileTagUtils.kt | 20 +++++++++---------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/main/kotlin/eu/ibagroup/formainframe/config/connect/ui/zosmf/ConnectionDialog.kt b/src/main/kotlin/eu/ibagroup/formainframe/config/connect/ui/zosmf/ConnectionDialog.kt index c4ad1a329..2fbea1bf9 100644 --- a/src/main/kotlin/eu/ibagroup/formainframe/config/connect/ui/zosmf/ConnectionDialog.kt +++ b/src/main/kotlin/eu/ibagroup/formainframe/config/connect/ui/zosmf/ConnectionDialog.kt @@ -47,13 +47,10 @@ import eu.ibagroup.formainframe.dataops.operations.ChangePasswordOperation import eu.ibagroup.formainframe.dataops.operations.InfoOperation import eu.ibagroup.formainframe.dataops.operations.ZOSInfoOperation import eu.ibagroup.formainframe.explorer.EXPLORER_NOTIFICATION_GROUP_ID +import eu.ibagroup.formainframe.utils.* import eu.ibagroup.formainframe.utils.crudable.Crudable import eu.ibagroup.formainframe.utils.crudable.find import eu.ibagroup.formainframe.utils.crudable.getAll -import eu.ibagroup.formainframe.utils.runTask -import eu.ibagroup.formainframe.utils.validateConnectionName -import eu.ibagroup.formainframe.utils.validateForBlank -import eu.ibagroup.formainframe.utils.validateZosmfUrl import org.zowe.kotlinsdk.ChangePassword import org.zowe.kotlinsdk.annotations.ZVersion import java.awt.Component diff --git a/src/main/kotlin/eu/ibagroup/formainframe/ui/build/jobs/JobBuildTreeView.kt b/src/main/kotlin/eu/ibagroup/formainframe/ui/build/jobs/JobBuildTreeView.kt index d4375b566..254ad59c6 100644 --- a/src/main/kotlin/eu/ibagroup/formainframe/ui/build/jobs/JobBuildTreeView.kt +++ b/src/main/kotlin/eu/ibagroup/formainframe/ui/build/jobs/JobBuildTreeView.kt @@ -48,6 +48,9 @@ import javax.swing.tree.TreeNode val JOBS_LOG_VIEW = DataKey.create("jobsLogView") const val JOBS_LOG_NOTIFICATION_GROUP_ID = "eu.ibagroup.formainframe.explorer.ExplorerNotificationGroup" +const val SUCCESSFUL_JOB_COMPLETION_CODE = 0 +const val SUCCESSFUL_JOB_COMPLETION_CODE_WITH_WARNING = 4 + /** * Console with BuildTree for display job execution process and results. * @param jobLogInfo job process information necessary to get log and status. @@ -144,7 +147,7 @@ class JobBuildTreeView( } else ReturnCode.ERROR jobLogger.fetchLog() - var finalLogFiles = jobLogger.logFetcher.getCachedLog() + val finalLogFiles = jobLogger.logFetcher.getCachedLog() if (finalLogFiles.count() != spoolFileToLogMap.count()) { onNextLog() } diff --git a/src/main/kotlin/eu/ibagroup/formainframe/utils/ussFileTagUtils.kt b/src/main/kotlin/eu/ibagroup/formainframe/utils/ussFileTagUtils.kt index ad238a878..a67e1425c 100644 --- a/src/main/kotlin/eu/ibagroup/formainframe/utils/ussFileTagUtils.kt +++ b/src/main/kotlin/eu/ibagroup/formainframe/utils/ussFileTagUtils.kt @@ -15,10 +15,6 @@ package eu.ibagroup.formainframe.utils import com.ibm.mq.headers.CCSID -import com.intellij.notification.Notification -import com.intellij.notification.NotificationType -import com.intellij.notification.Notifications -import com.intellij.openapi.components.service import eu.ibagroup.formainframe.common.message import eu.ibagroup.formainframe.config.connect.ConnectionConfig import eu.ibagroup.formainframe.dataops.DataOpsManager @@ -26,6 +22,7 @@ import eu.ibagroup.formainframe.dataops.attributes.RemoteUssAttributes import eu.ibagroup.formainframe.dataops.content.synchronizer.DEFAULT_BINARY_CHARSET import eu.ibagroup.formainframe.dataops.operations.uss.ChangeFileTagOperation import eu.ibagroup.formainframe.dataops.operations.uss.ChangeFileTagOperationParams +import eu.ibagroup.formainframe.telemetry.NotificationCompatibleException import eu.ibagroup.formainframe.telemetry.NotificationsService import okhttp3.ResponseBody import okhttp3.internal.indexOfNonWhitespace @@ -44,9 +41,11 @@ fun checkUssFileTag(attributes: RemoteUssAttributes) { if (getSupportedEncodings().contains(charset)) { attributes.charset = charset } else { - notifyError( - message("filetag.unsupported.encoding.error.title", charset), - message("filetag.unsupported.encoding.error.message", DEFAULT_BINARY_CHARSET.name()) + NotificationsService.errorNotification( + NotificationCompatibleException( + message("filetag.unsupported.encoding.error.title", charset), + message("filetag.unsupported.encoding.error.message", DEFAULT_BINARY_CHARSET.name()) + ) ) attributes.charset = DEFAULT_BINARY_CHARSET } @@ -95,9 +94,10 @@ fun getUssFileTagCharset(attributes: RemoteUssAttributes): Charset? { runCatching { return Charset.forName(tagCharset) }.onFailure { - notifyError( - message("filetag.encoding.detection.error.title"), - message("filetag.encoding.detection.error.message", DEFAULT_BINARY_CHARSET.name()), + NotificationsService.errorNotification( + it, + custTitle = message("filetag.encoding.detection.error.title"), + custDetailsShort = message("filetag.encoding.detection.error.message", DEFAULT_BINARY_CHARSET.name()), ) } }