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 786c6fbf..8f857e3a 100644 --- a/src/main/kotlin/eu/ibagroup/formainframe/dataops/attributes/RemoteUssAttributes.kt +++ b/src/main/kotlin/eu/ibagroup/formainframe/dataops/attributes/RemoteUssAttributes.kt @@ -76,9 +76,9 @@ data class RemoteUssAttributes( override val requesters: MutableList, override val length: Long = 0L, val uid: Long? = null, - val owner: String? = null, + var owner: String? = null, val gid: Long? = null, - val groupId: String? = null, + var groupId: String? = null, val modificationTime: String? = null, val symlinkTarget: String? = null, var charset: Charset = DEFAULT_BINARY_CHARSET diff --git a/src/main/kotlin/eu/ibagroup/formainframe/dataops/operations/UssChangeOwner.kt b/src/main/kotlin/eu/ibagroup/formainframe/dataops/operations/UssChangeOwner.kt new file mode 100644 index 00000000..a558b9b9 --- /dev/null +++ b/src/main/kotlin/eu/ibagroup/formainframe/dataops/operations/UssChangeOwner.kt @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2020-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.dataops.operations + +import com.intellij.openapi.progress.ProgressIndicator +import eu.ibagroup.formainframe.api.api +import eu.ibagroup.formainframe.config.connect.ConnectionConfig +import eu.ibagroup.formainframe.config.connect.authToken +import eu.ibagroup.formainframe.dataops.DataOpsManager +import eu.ibagroup.formainframe.dataops.exceptions.CallException +import eu.ibagroup.formainframe.utils.cancelByIndicator +import eu.ibagroup.formainframe.utils.log +import org.zowe.kotlinsdk.ChangeOwner +import org.zowe.kotlinsdk.DataAPI +import org.zowe.kotlinsdk.FilePath + +/** + * Class which represents factory for uss change owner operation runner. Defined in plugin.xml + */ +class UssChangeOwnerFactory : OperationRunnerFactory { + override fun buildComponent(dataOpsManager: DataOpsManager): OperationRunner { + return UssChangeOwner() + } +} + +/** + * Data class which represents input parameters for uss change owner operation + * @param parameters instance of [ChangeOwner] object + * @param path path of uss file + */ +data class UssChangeOwnerParams( + val parameters: ChangeOwner, + val path: String, +) + +/** + * Data class which represents uss change owner operation object + */ +data class UssChangeOwnerOperation( + override val request: UssChangeOwnerParams, + override val connectionConfig: ConnectionConfig, +) : RemoteUnitOperation + +/** + * Class which represents uss change owner operation runner + */ +class UssChangeOwner : OperationRunner { + + override val operationClass = UssChangeOwnerOperation::class.java + override val resultClass = Unit::class.java + override val log = log() + + /** + * Runs an uss change owner operation + * @param operation uss change owner operation to be run + * @param progressIndicator progress indicator object + * @throws CallException if request is not successful + * @return Void + */ + override fun run( + operation: UssChangeOwnerOperation, progressIndicator: ProgressIndicator + ) { + progressIndicator.checkCanceled() + val response = api(operation.connectionConfig).changeFileOwner( + authorizationToken = operation.connectionConfig.authToken, + filePath = FilePath(operation.request.path), + body = operation.request.parameters + ).cancelByIndicator(progressIndicator).execute() + if (!response.isSuccessful) { + throw CallException( + response, "Cannot change file owner on ${operation.request.path}" + ) + } + } + + override fun canRun(operation: UssChangeOwnerOperation): Boolean { + return true + } +} diff --git a/src/main/kotlin/eu/ibagroup/formainframe/explorer/actions/GetFilePropertiesAction.kt b/src/main/kotlin/eu/ibagroup/formainframe/explorer/actions/GetFilePropertiesAction.kt index ddf9395f..418d338c 100644 --- a/src/main/kotlin/eu/ibagroup/formainframe/explorer/actions/GetFilePropertiesAction.kt +++ b/src/main/kotlin/eu/ibagroup/formainframe/explorer/actions/GetFilePropertiesAction.kt @@ -25,6 +25,8 @@ import eu.ibagroup.formainframe.dataops.attributes.RemoteMemberAttributes import eu.ibagroup.formainframe.dataops.attributes.RemoteUssAttributes import eu.ibagroup.formainframe.dataops.operations.UssChangeModeOperation import eu.ibagroup.formainframe.dataops.operations.UssChangeModeParams +import eu.ibagroup.formainframe.dataops.operations.UssChangeOwnerOperation +import eu.ibagroup.formainframe.dataops.operations.UssChangeOwnerParams import eu.ibagroup.formainframe.explorer.ExplorerUnit import eu.ibagroup.formainframe.explorer.ui.* import eu.ibagroup.formainframe.telemetry.NotificationsService @@ -32,6 +34,7 @@ import eu.ibagroup.formainframe.utils.changeFileEncodingAction import eu.ibagroup.formainframe.utils.clone import eu.ibagroup.formainframe.utils.isBeingEditingNow import org.zowe.kotlinsdk.ChangeMode +import org.zowe.kotlinsdk.ChangeOwner /** * Action for displaying properties of files on UI in dialog by clicking item in explorer context menu. @@ -83,32 +86,64 @@ class GetFilePropertiesAction : AnAction() { // } val oldCharset = attributes.charset val initFileMode = attributes.fileMode?.clone() + val initOwner = attributes.owner?.clone() + val initGroupID = attributes.groupId?.clone() val dialog = UssFilePropertiesDialog(project, UssFileState(attributes, virtualFile.isBeingEditingNow())) if (dialog.showAndGet()) { - if ( - attributes.fileMode?.owner != initFileMode?.owner - || attributes.fileMode?.group != initFileMode?.group - || attributes.fileMode?.all != initFileMode?.all - ) { - runBackgroundableTask( - title = "Changing file mode on ${attributes.path}", - project = project, - cancellable = true - ) { - if (attributes.fileMode != null) { - runCatching { - dataOpsManager.performOperation( - operation = UssChangeModeOperation( - request = UssChangeModeParams(ChangeMode(mode = attributes.fileMode), attributes.path), - connectionConfig = connectionConfig - ), - progressIndicator = it - ) - }.onFailure { t -> - initFileMode?.owner?.let { attributes.fileMode.owner = it } - initFileMode?.group?.let { attributes.fileMode.group = it } - initFileMode?.all?.let { attributes.fileMode.all = it } - NotificationsService.errorNotification(t, e.project) + val isFileModeChanged = attributes.fileMode?.owner != initFileMode?.owner + || attributes.fileMode?.group != initFileMode?.group + || attributes.fileMode?.all != initFileMode?.all + val isOwnerChanged = attributes.owner != initOwner || attributes.groupId != initGroupID + if (isFileModeChanged || isOwnerChanged) { + if (isOwnerChanged) { + runBackgroundableTask( + title = "Changing file owner on ${attributes.path}", + project = project, + cancellable = true + ) { + if (attributes.owner != null || attributes.groupId != null) { + runCatching { + dataOpsManager.performOperation( + operation = UssChangeOwnerOperation( + request = UssChangeOwnerParams( + ChangeOwner( + owner = attributes.owner ?: "", + group = attributes.groupId ?: "" + ), attributes.path + ), + connectionConfig = connectionConfig + ), + progressIndicator = it + ) + }.onFailure { t -> + initOwner?.let { attributes.owner = it } + initGroupID?.let { attributes.groupId = it } + NotificationsService.errorNotification(t, e.project) + } + } + } + } + if (isFileModeChanged) { + runBackgroundableTask( + title = "Changing file mode on ${attributes.path}", + project = project, + cancellable = true + ) { + if (attributes.fileMode != null) { + runCatching { + dataOpsManager.performOperation( + operation = UssChangeModeOperation( + request = UssChangeModeParams(ChangeMode(mode = attributes.fileMode), attributes.path), + connectionConfig = connectionConfig + ), + progressIndicator = it + ) + }.onFailure { t -> + initFileMode?.owner?.let { attributes.fileMode.owner = it } + initFileMode?.group?.let { attributes.fileMode.group = it } + initFileMode?.all?.let { attributes.fileMode.all = it } + NotificationsService.errorNotification(t, e.project) + } } node.parent?.cleanCacheIfPossible(cleanBatchedQuery = false) } diff --git a/src/main/kotlin/eu/ibagroup/formainframe/explorer/ui/UssFilePropertiesDialog.kt b/src/main/kotlin/eu/ibagroup/formainframe/explorer/ui/UssFilePropertiesDialog.kt index c142e112..2a18abb6 100644 --- a/src/main/kotlin/eu/ibagroup/formainframe/explorer/ui/UssFilePropertiesDialog.kt +++ b/src/main/kotlin/eu/ibagroup/formainframe/explorer/ui/UssFilePropertiesDialog.kt @@ -18,6 +18,7 @@ import com.intellij.openapi.project.Project import com.intellij.openapi.ui.DialogPanel import com.intellij.openapi.ui.ComboBox import com.intellij.openapi.ui.DialogWrapper +import com.intellij.openapi.ui.ValidationInfo import com.intellij.ui.components.JBTabbedPane import com.intellij.ui.dsl.builder.bindItem import com.intellij.ui.dsl.builder.panel @@ -27,8 +28,7 @@ import eu.ibagroup.formainframe.dataops.attributes.RemoteUssAttributes import javax.swing.JComponent import com.intellij.ui.dsl.builder.* import eu.ibagroup.formainframe.dataops.content.synchronizer.DEFAULT_BINARY_CHARSET -import eu.ibagroup.formainframe.utils.getParamTextValueOrUnknown -import eu.ibagroup.formainframe.utils.getSupportedEncodings +import eu.ibagroup.formainframe.utils.* import org.zowe.kotlinsdk.* import java.nio.charset.Charset @@ -38,37 +38,8 @@ class UssFilePropertiesDialog(project: Project?, override var state: UssFileStat private val sameWidthGroup = "USS_FILE_PROPERTIES_DIALOG_LABELS_WIDTH_GROUP" - private lateinit var generalTab: DialogPanel - - private lateinit var permissionTab: DialogPanel - - private lateinit var comboBox: Cell> - - var fileTypeName: String = "File" - - private val fileModeValues = listOf( - FileModeValue.NONE, - FileModeValue.EXECUTE, - FileModeValue.WRITE, - FileModeValue.WRITE_EXECUTE, - FileModeValue.READ, - FileModeValue.READ_EXECUTE, - FileModeValue.READ_WRITE, - FileModeValue.READ_WRITE_EXECUTE - ) - - init { - - if (state.ussAttributes.isDirectory) - fileTypeName = "Directory" - title = "$fileTypeName Properties" - init() - } - - override fun createCenterPanel(): JComponent { - val tabbedPanel = JBTabbedPane() - - generalTab = panel { + private val generalTab by lazy{ + panel { row { label("$fileTypeName name: ") .widthGroup(sameWidthGroup) @@ -134,22 +105,30 @@ class UssFilePropertiesDialog(project: Project?, override var state: UssFileStat } } } + } - permissionTab = panel { + private val permissionTab by lazy{ + panel { row { label("Owner: ") .widthGroup(sameWidthGroup) textField() - .text(getParamTextValueOrUnknown(state.ussAttributes.owner)) - .applyToComponent { isEditable = false } + .bindText( + { state.ussAttributes.owner ?: UNKNOWN_PARAM_VALUE }, + { state.ussAttributes.owner = it } + ) + .validationOnApply { validateForBlank(it) ?: validateFieldWithLengthRestriction(it, 8, "Owner") } .align(AlignX.FILL) } row { label("Group: ") .widthGroup(sameWidthGroup) textField() - .text(getParamTextValueOrUnknown(state.ussAttributes.groupId)) - .applyToComponent { isEditable = false } + .bindText( + { state.ussAttributes.groupId ?: UNKNOWN_PARAM_VALUE }, + { state.ussAttributes.groupId = it } + ) + .validationOnApply { validateForBlank(it) ?: validateFieldWithLengthRestriction(it, 8, "Group") } .align(AlignX.FILL) } row { @@ -191,7 +170,35 @@ class UssFilePropertiesDialog(project: Project?, override var state: UssFileStat .align(AlignX.FILL) } } + } + + private lateinit var comboBox: Cell> + + var fileTypeName: String = "File" + + private val fileModeValues = listOf( + FileModeValue.NONE, + FileModeValue.EXECUTE, + FileModeValue.WRITE, + FileModeValue.WRITE_EXECUTE, + FileModeValue.READ, + FileModeValue.READ_EXECUTE, + FileModeValue.READ_WRITE, + FileModeValue.READ_WRITE_EXECUTE + ) + + init { + if (state.ussAttributes.isDirectory) + fileTypeName = "Directory" + title = "$fileTypeName Properties" + permissionTab.registerValidators(myDisposable) { map -> + isOKActionEnabled = map.isEmpty() + } + init() + } + override fun createCenterPanel(): JComponent { + val tabbedPanel = JBTabbedPane() tabbedPanel.add("General", generalTab) tabbedPanel.add("Permissions", permissionTab) @@ -204,6 +211,13 @@ class UssFilePropertiesDialog(project: Project?, override var state: UssFileStat super.doOKAction() } + /** + * Overloaded method to validate components in the permissionTab panel + */ + override fun doValidate(): ValidationInfo? { + return permissionTab.validateAll().firstOrNull() ?: super.doValidate() + } + } class UssFileState(var ussAttributes: RemoteUssAttributes, val fileIsBeingEditingNow: Boolean) diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index b9d84a0a..a47f272a 100755 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -193,6 +193,8 @@ Example of how to see the output:
+ + diff --git a/src/test/kotlin/eu/ibagroup/formainframe/dataops/operations/UssChangeOwnerTestSpec.kt b/src/test/kotlin/eu/ibagroup/formainframe/dataops/operations/UssChangeOwnerTestSpec.kt new file mode 100644 index 00000000..1762b114 --- /dev/null +++ b/src/test/kotlin/eu/ibagroup/formainframe/dataops/operations/UssChangeOwnerTestSpec.kt @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2020-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.dataops.operations + +import com.intellij.openapi.progress.ProgressIndicator +import eu.ibagroup.formainframe.api.ZosmfApi +import eu.ibagroup.formainframe.config.connect.ConnectionConfig +import eu.ibagroup.formainframe.config.connect.authToken +import eu.ibagroup.formainframe.dataops.attributes.MaskedRequester +import eu.ibagroup.formainframe.dataops.attributes.UssRequester +import eu.ibagroup.formainframe.dataops.exceptions.CallException +import eu.ibagroup.formainframe.testutils.WithApplicationShouldSpec +import eu.ibagroup.formainframe.testutils.testServiceImpl.TestZosmfApiImpl +import eu.ibagroup.formainframe.utils.cancelByIndicator +import io.kotest.assertions.assertSoftly +import io.kotest.assertions.throwables.shouldThrowExactly +import io.kotest.matchers.shouldBe +import io.mockk.* +import org.zowe.kotlinsdk.ChangeOwner +import org.zowe.kotlinsdk.DataAPI +import org.zowe.kotlinsdk.FilePath +import retrofit2.Response + +class UssChangeOwnerTestSpec : WithApplicationShouldSpec({ + beforeSpec { + clearAllMocks() + } + + context("UssChangeOwnerOperationRunner common spec") { + + val dataApi = mockk() + val zosmfApi = ZosmfApi.getService() as TestZosmfApiImpl + zosmfApi.testInstance = object : TestZosmfApiImpl() { + override fun getApi(apiClass: Class, connectionConfig: ConnectionConfig): Api { + @Suppress("UNCHECKED_CAST") return dataApi as Api + } + } + + val classUnderTest = spyk(UssChangeOwner()) + val operation = mockk() + + context("canRun") { + + should("returnTrue_whenCanRun_givenRemoteMemberAttributes") { + + val canRun = classUnderTest.canRun(operation) + + assertSoftly { + canRun shouldBe true + } + } + + should("returnTrue_whenCanRun_givenRemoteDatasetAttributes") { + + val canRun = classUnderTest.canRun(operation) + + assertSoftly { + canRun shouldBe true + } + } + + should("returnTrue_whenCanRun_givenRemoteUssAttributes") { + + val canRun = classUnderTest.canRun(operation) + + assertSoftly { + canRun shouldBe true + } + } + } + + context("run operation") { + + val progressIndicator = mockk() + val datasetRequester = mockk() + val ussRequester = mockk() + val connectionConfig = mockk() + val ussChangeOwnerParams = UssChangeOwnerParams( + ChangeOwner( + owner = "owner", group = "group" + ), "attributes/path" + ) + + mockkStatic("eu.ibagroup.formainframe.config.connect.CredentialServiceKt") + every { connectionConfig.uuid } returns "00000000" + every { connectionConfig.authToken } returns "TEST_TOKEN" + every { datasetRequester.connectionConfig } returns connectionConfig + every { ussRequester.connectionConfig } returns connectionConfig + every { progressIndicator.checkCanceled() } just Runs + every { operation.connectionConfig } returns connectionConfig + every { operation.request } returns ussChangeOwnerParams + val apiResponse = mockk>() + every { + dataApi.changeFileOwner(any(), any(), any(), any()).cancelByIndicator(progressIndicator).execute() + } returns apiResponse + + should("successfully run changeFileOwner operation") { + every { apiResponse.isSuccessful } returns true + + classUnderTest.run(operation, progressIndicator) + + verify(exactly = 1) { + dataApi.changeFileOwner( + "TEST_TOKEN", null, ChangeOwner( + owner = "owner", group = "group" + ), FilePath("attributes/path") + ) + } + } + + should("failed run changeFileOwner operation") { + every { apiResponse.isSuccessful } returns false + every { apiResponse.code() } returns 500 + val exception = shouldThrowExactly { + classUnderTest.run(operation, progressIndicator) + } + assertSoftly { + exception.message shouldBe "Cannot change file owner on attributes/path\nCode: 500" + } + } + + } + + + } +}) \ No newline at end of file