From 1ef06b7892d5de8ea0c046537c629240e49359ef Mon Sep 17 00:00:00 2001 From: Uladzislau Kalesnikau Date: Thu, 7 Nov 2024 17:56:45 +0100 Subject: [PATCH] IJMP-1748 Requesters workaround --- gradle/wrapper/gradle-wrapper.properties | 2 +- .../formainframe/dataops/Operation.kt | 4 +- .../MFRemoteAttributesServiceBase.kt | 3 +- .../dataops/attributes/MaskedRequester.kt | 3 +- .../attributes/RemoteDatasetAttributes.kt | 8 +- .../dataops/attributes/RemoteJobAttributes.kt | 7 +- .../dataops/attributes/RemoteUssAttributes.kt | 6 +- .../dataops/attributes/Requester.kt | 9 +- .../dataops/fetch/UssFileFetchProvider.kt | 21 +- .../operations/DeleteOperationRunner.kt | 174 ++++++++-------- ...emberOrUssFileOrSequentialToUssDirMover.kt | 22 ++- .../operations/mover/UssToUssFileMover.kt | 20 +- .../actions/GetFilePropertiesAction.kt | 6 +- .../explorer/actions/RenameAction.kt | 69 +++++-- .../actions/SubmitJobToolbarAction.kt | 4 +- .../ui/CommonExplorerTreeStructure.kt | 15 +- .../formainframe/explorer/ui/DSMaskNode.kt | 3 + .../explorer/ui/ExplorerPasteProvider.kt | 8 +- .../explorer/ui/ExplorerTreeView.kt | 4 +- .../explorer/ui/FileExplorerView.kt | 8 +- .../utils/crudable/annotations/Column.kt | 2 +- .../formainframe/v3/ConnectionConfig.kt | 69 +++++++ .../formainframe/v3/EntityWithUuid.kt | 43 ++++ .../eu/ibagroup/formainframe/v3/Requester.kt | 23 +++ .../ibagroup/formainframe/v3/UssRequester.kt | 20 ++ .../v3/operations/OperationData.kt | 28 +++ .../v3/operations/OperationRunner.kt | 46 +++++ .../v3/operations/OperationsService.kt | 89 +++++++++ .../v3/operations/RenameOperationData.kt | 33 ++++ .../v3/operations/RenameOperationRunner.kt | 186 ++++++++++++++++++ .../v3/operations/UnitOperationData.kt | 25 +++ .../v3/operations/UnitOperationRunner.kt | 30 +++ src/main/resources/META-INF/plugin.xml | 6 + .../explorer/actions/RenameActionTestSpec.kt | 121 ++++++++++-- .../ui/ExplorerPasteProviderTestSpec.kt | 27 +-- 35 files changed, 959 insertions(+), 185 deletions(-) create mode 100644 src/main/kotlin/eu/ibagroup/formainframe/v3/ConnectionConfig.kt create mode 100644 src/main/kotlin/eu/ibagroup/formainframe/v3/EntityWithUuid.kt create mode 100644 src/main/kotlin/eu/ibagroup/formainframe/v3/Requester.kt create mode 100644 src/main/kotlin/eu/ibagroup/formainframe/v3/UssRequester.kt create mode 100644 src/main/kotlin/eu/ibagroup/formainframe/v3/operations/OperationData.kt create mode 100644 src/main/kotlin/eu/ibagroup/formainframe/v3/operations/OperationRunner.kt create mode 100644 src/main/kotlin/eu/ibagroup/formainframe/v3/operations/OperationsService.kt create mode 100644 src/main/kotlin/eu/ibagroup/formainframe/v3/operations/RenameOperationData.kt create mode 100644 src/main/kotlin/eu/ibagroup/formainframe/v3/operations/RenameOperationRunner.kt create mode 100644 src/main/kotlin/eu/ibagroup/formainframe/v3/operations/UnitOperationData.kt create mode 100644 src/main/kotlin/eu/ibagroup/formainframe/v3/operations/UnitOperationRunner.kt diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b82aa23a4..df97d72b8 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/src/main/kotlin/eu/ibagroup/formainframe/dataops/Operation.kt b/src/main/kotlin/eu/ibagroup/formainframe/dataops/Operation.kt index 69b2ec6e8..cfb44d07c 100644 --- a/src/main/kotlin/eu/ibagroup/formainframe/dataops/Operation.kt +++ b/src/main/kotlin/eu/ibagroup/formainframe/dataops/Operation.kt @@ -15,8 +15,8 @@ package eu.ibagroup.formainframe.dataops /** - * Interface to describe an operation in plugin. - * @param Result result that should be returned after operation execution. + * Interface to describe an operation in the plugin + * @property resultClass the result class of the result that should be returned after an operation execution */ interface Operation { diff --git a/src/main/kotlin/eu/ibagroup/formainframe/dataops/attributes/MFRemoteAttributesServiceBase.kt b/src/main/kotlin/eu/ibagroup/formainframe/dataops/attributes/MFRemoteAttributesServiceBase.kt index dbb3a45ae..dddddb052 100644 --- a/src/main/kotlin/eu/ibagroup/formainframe/dataops/attributes/MFRemoteAttributesServiceBase.kt +++ b/src/main/kotlin/eu/ibagroup/formainframe/dataops/attributes/MFRemoteAttributesServiceBase.kt @@ -184,7 +184,8 @@ abstract class MFRemoteAttributesServiceBase { val connectionConfig: Connection } diff --git a/src/main/kotlin/eu/ibagroup/formainframe/dataops/fetch/UssFileFetchProvider.kt b/src/main/kotlin/eu/ibagroup/formainframe/dataops/fetch/UssFileFetchProvider.kt index 3ea3569b1..61c27467c 100644 --- a/src/main/kotlin/eu/ibagroup/formainframe/dataops/fetch/UssFileFetchProvider.kt +++ b/src/main/kotlin/eu/ibagroup/formainframe/dataops/fetch/UssFileFetchProvider.kt @@ -81,16 +81,17 @@ class UssFileFetchProvider( .execute() if (response.isSuccessful) { - attributes = response.body()?.items?.filter { - it.name != UPPER_DIR_NAME - }?.map { - RemoteUssAttributes( - rootPath = query.request.path, - ussFile = it, - url = query.connectionConfig.url, - connectionConfig = query.connectionConfig - ) - } + attributes = response.body() + ?.items + ?.filter { it.name != UPPER_DIR_NAME } + ?.map { + RemoteUssAttributes( + rootPath = query.request.path, + ussFile = it, + url = query.connectionConfig.url, + connectionConfig = query.connectionConfig + ) + } log.info("${query.request} returned ${attributes?.size ?: 0} entities") log.debug { attributes?.joinToString("\n") ?: "" diff --git a/src/main/kotlin/eu/ibagroup/formainframe/dataops/operations/DeleteOperationRunner.kt b/src/main/kotlin/eu/ibagroup/formainframe/dataops/operations/DeleteOperationRunner.kt index 053236eb6..469918943 100644 --- a/src/main/kotlin/eu/ibagroup/formainframe/dataops/operations/DeleteOperationRunner.kt +++ b/src/main/kotlin/eu/ibagroup/formainframe/dataops/operations/DeleteOperationRunner.kt @@ -20,14 +20,11 @@ import eu.ibagroup.formainframe.analytics.AnalyticsService import eu.ibagroup.formainframe.analytics.events.FileAction import eu.ibagroup.formainframe.analytics.events.FileEvent 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.UnitOperation -import eu.ibagroup.formainframe.dataops.attributes.FileAttributes -import eu.ibagroup.formainframe.dataops.attributes.RemoteDatasetAttributes -import eu.ibagroup.formainframe.dataops.attributes.RemoteMemberAttributes -import eu.ibagroup.formainframe.dataops.attributes.RemoteUssAttributes -import eu.ibagroup.formainframe.dataops.attributes.getLibraryAttributes +import eu.ibagroup.formainframe.dataops.attributes.* import eu.ibagroup.formainframe.dataops.exceptions.CallException import eu.ibagroup.formainframe.utils.cancelByIndicator import eu.ibagroup.formainframe.utils.findAnyNullable @@ -36,6 +33,7 @@ import eu.ibagroup.formainframe.utils.runWriteActionInEdt import org.zowe.kotlinsdk.DataAPI import org.zowe.kotlinsdk.FilePath import org.zowe.kotlinsdk.XIBMOption +import retrofit2.Call class DeleteRunnerFactory : OperationRunnerFactory { override fun buildComponent(dataOpsManager: DataOpsManager): OperationRunner<*, *> { @@ -43,8 +41,52 @@ class DeleteRunnerFactory : OperationRunnerFactory { } } -class DeleteOperationRunner(private val dataOpsManager: DataOpsManager) : - OperationRunner { +/** + * Send delete operation call and delete the respective element from the virtual file system + * @param operation the delete operation instance + * @param opRunner the operation runner + * @param progressIndicator the progress indicator to cancel the operation by on the call end + * @param requesters the requesters to perform the operation for + * @param deleteOperationCallBuilder the operation call builder to build the call + * @param exceptionMsg the exception message to put in the [CallException] if the operation was not successful + */ +private fun processDeleteForRequesters( + operation: DeleteOperation, + opRunner: DeleteOperationRunner, + progressIndicator: ProgressIndicator, + requesters: List>, + deleteOperationCallBuilder: (ConnectionConfig) -> Call, + exceptionMsg: String = "Cannot delete the element" +) { + var throwable: Throwable? = null + requesters + .stream() + .map { requester -> + try { + progressIndicator.checkCanceled() + val response = deleteOperationCallBuilder(requester.connectionConfig) + .cancelByIndicator(progressIndicator) + .execute() + if (response.isSuccessful) { + runWriteActionInEdt { operation.file.delete(opRunner) } + true + } else { + throwable = CallException(response, exceptionMsg) + false + } + } catch (t: Throwable) { + throwable = t + false + } + } + .filter { it } + .findAnyNullable() + ?: throw (throwable ?: Throwable("Unknown")) +} + +class DeleteOperationRunner( + private val dataOpsManager: DataOpsManager +) : OperationRunner { override val operationClass = DeleteOperation::class.java override val log = log() @@ -59,97 +101,75 @@ class DeleteOperationRunner(private val dataOpsManager: DataOpsManager) : operation: DeleteOperation, progressIndicator: ProgressIndicator ) { - when (val attr = operation.attributes) { - is RemoteDatasetAttributes -> { - AnalyticsService.getService().trackAnalyticsEvent(FileEvent(attr, FileAction.DELETE)) + val attr = operation.attributes + AnalyticsService.getService().trackAnalyticsEvent(FileEvent(attr, FileAction.DELETE)) + when (attr) { + is RemoteDatasetAttributes -> { if (operation.file.children != null) { operation.file.children.forEach { it.isWritable = false } } else { operation.file.isWritable = false } - var throwable: Throwable? = null - attr.requesters.stream().map { - try { - progressIndicator.checkCanceled() - val response = api(it.connectionConfig).deleteDataset( - authorizationToken = it.connectionConfig.authToken, - datasetName = attr.name - ).cancelByIndicator(progressIndicator).execute() - if (response.isSuccessful) { - runWriteActionInEdt { operation.file.delete(this@DeleteOperationRunner) } - true - } else { - throwable = CallException(response, "Cannot delete data set") - false - } - } catch (t: Throwable) { - throwable = t - false - } - }.filter { it }.findAnyNullable() ?: throw (throwable ?: Throwable("Unknown")) + val deleteOperationCallBuilder = { connectionConfig: ConnectionConfig -> + api(connectionConfig).deleteDataset( + authorizationToken = connectionConfig.authToken, + datasetName = attr.name + ) + } + processDeleteForRequesters( + operation, + this, + progressIndicator, + attr.requesters, + deleteOperationCallBuilder, + "Cannot delete data set" + ) } is RemoteMemberAttributes -> { - AnalyticsService.getService().trackAnalyticsEvent(FileEvent(attr, FileAction.DELETE)) - operation.file.isWritable = false val libraryAttributes = attr.getLibraryAttributes(dataOpsManager) if (libraryAttributes != null) { - var throwable: Throwable? = null - libraryAttributes.requesters.stream().map { - try { - progressIndicator.checkCanceled() - val response = api(it.connectionConfig).deleteDatasetMember( - authorizationToken = it.connectionConfig.authToken, - datasetName = libraryAttributes.name, - memberName = attr.name - ).cancelByIndicator(progressIndicator).execute() - if (response.isSuccessful) { - runWriteActionInEdt { operation.file.delete(this@DeleteOperationRunner) } - true - } else { - throwable = CallException(response, "Cannot delete data set member") - false - } - } catch (t: Throwable) { - throwable = t - false - } - }.filter { it }.findAnyNullable() ?: throw (throwable ?: Throwable("Unknown")) + val deleteOperationCallBuilder = { connectionConfig: ConnectionConfig -> + api(connectionConfig).deleteDatasetMember( + authorizationToken = connectionConfig.authToken, + datasetName = libraryAttributes.name, + memberName = attr.name + ) + } + processDeleteForRequesters( + operation, + this, + progressIndicator, + libraryAttributes.requesters, + deleteOperationCallBuilder, + "Cannot delete data set member" + ) } } is RemoteUssAttributes -> { - AnalyticsService.getService().trackAnalyticsEvent(FileEvent(attr, FileAction.DELETE)) - if (operation.file.isDirectory) { operation.file.children.forEach { it.isWritable = false } } else { operation.file.isWritable = false } - var throwable: Throwable? = null - attr.requesters.stream().map { - try { - progressIndicator.checkCanceled() - val response = api(it.connectionConfig).deleteUssFile( - authorizationToken = it.connectionConfig.authToken, - filePath = FilePath(attr.path), - xIBMOption = XIBMOption.RECURSIVE - ).cancelByIndicator(progressIndicator).execute() - if (response.isSuccessful) { - // TODO: clarify issue with removing from MF Virtual file system - // runWriteActionInEdt { operation.file.delete(this@DeleteOperationRunner) } - true - } else { - throwable = CallException(response, "Cannot delete USS File/Directory") - false - } - } catch (t: Throwable) { - throwable = t - false - } - }.filter { it }.findAnyNullable() ?: throw (throwable ?: Throwable("Unknown")) + val deleteOperationCallBuilder = { connectionConfig: ConnectionConfig -> + api(connectionConfig).deleteUssFile( + authorizationToken = connectionConfig.authToken, + filePath = FilePath(attr.path), + xIBMOption = XIBMOption.RECURSIVE + ) + } + processDeleteForRequesters( + operation, + this, + progressIndicator, + attr.requesters, + deleteOperationCallBuilder, + "Cannot delete USS File/Directory" + ) } } } diff --git a/src/main/kotlin/eu/ibagroup/formainframe/dataops/operations/mover/CrossSystemMemberOrUssFileOrSequentialToUssDirMover.kt b/src/main/kotlin/eu/ibagroup/formainframe/dataops/operations/mover/CrossSystemMemberOrUssFileOrSequentialToUssDirMover.kt index f70b09869..34082590f 100644 --- a/src/main/kotlin/eu/ibagroup/formainframe/dataops/operations/mover/CrossSystemMemberOrUssFileOrSequentialToUssDirMover.kt +++ b/src/main/kotlin/eu/ibagroup/formainframe/dataops/operations/mover/CrossSystemMemberOrUssFileOrSequentialToUssDirMover.kt @@ -26,7 +26,10 @@ import eu.ibagroup.formainframe.dataops.exceptions.CallException import eu.ibagroup.formainframe.dataops.operations.DeleteOperation import eu.ibagroup.formainframe.dataops.operations.OperationRunner import eu.ibagroup.formainframe.dataops.operations.OperationRunnerFactory -import eu.ibagroup.formainframe.utils.* +import eu.ibagroup.formainframe.utils.applyIfNotNull +import eu.ibagroup.formainframe.utils.cancelByIndicator +import eu.ibagroup.formainframe.utils.log +import eu.ibagroup.formainframe.utils.setUssFileTag import eu.ibagroup.formainframe.vfs.MFVirtualFile import org.zowe.kotlinsdk.DataAPI import org.zowe.kotlinsdk.FilePath @@ -54,15 +57,16 @@ class CrossSystemMemberOrUssFileOrSequentialToUssDirMover(val dataOpsManager: Da * @see OperationRunner.canRun */ override fun canRun(operation: MoveCopyOperation): Boolean { - return !operation.source.isDirectory && - operation.destination.isDirectory && + return !operation.source.isDirectory + && operation.destination.isDirectory + && (operation.sourceAttributes is RemoteMemberAttributes - || operation.sourceAttributes is RemoteUssAttributes - || operation.sourceAttributes is RemoteDatasetAttributes) && - operation.destinationAttributes is RemoteUssAttributes && - operation.source is MFVirtualFile && - operation.destination is MFVirtualFile && - operation.commonUrls(dataOpsManager).isEmpty() + || operation.sourceAttributes is RemoteUssAttributes + || operation.sourceAttributes is RemoteDatasetAttributes) + && operation.destinationAttributes is RemoteUssAttributes + && operation.source is MFVirtualFile + && operation.destination is MFVirtualFile + && operation.commonUrls(dataOpsManager).isEmpty() } override val log = log() diff --git a/src/main/kotlin/eu/ibagroup/formainframe/dataops/operations/mover/UssToUssFileMover.kt b/src/main/kotlin/eu/ibagroup/formainframe/dataops/operations/mover/UssToUssFileMover.kt index af801358f..109072df5 100644 --- a/src/main/kotlin/eu/ibagroup/formainframe/dataops/operations/mover/UssToUssFileMover.kt +++ b/src/main/kotlin/eu/ibagroup/formainframe/dataops/operations/mover/UssToUssFileMover.kt @@ -51,10 +51,10 @@ class UssToUssFileMover(private val dataOpsManager: DataOpsManager) : AbstractFi override fun canRun(operation: MoveCopyOperation): Boolean { return operation.sourceAttributes is RemoteUssAttributes - && operation.destinationAttributes is RemoteUssAttributes - && operation.destinationAttributes.isDirectory - && operation.commonUrls(dataOpsManager).isNotEmpty() - && !operation.destination.getParentsChain().containsAll(operation.source.getParentsChain()) + && operation.destinationAttributes is RemoteUssAttributes + && operation.destinationAttributes.isDirectory + && operation.commonUrls(dataOpsManager).isNotEmpty() + && !operation.destination.getParentsChain().containsAll(operation.source.getParentsChain()) } override val log = log() @@ -86,8 +86,8 @@ class UssToUssFileMover(private val dataOpsManager: DataOpsManager) : AbstractFi connectionConfig: ConnectionConfig, operation: MoveCopyOperation, from: String, - to: String) - : Pair, () -> Unit> { + to: String + ) : Pair, () -> Unit> { val copyCall = buildCopyCall(connectionConfig, operation, from, to).first val deleteSourceCallback = buildDeleteSourceCallback(operation) return Pair(copyCall, deleteSourceCallback) @@ -101,8 +101,8 @@ class UssToUssFileMover(private val dataOpsManager: DataOpsManager) : AbstractFi connectionConfig: ConnectionConfig, operation: MoveCopyOperation, from: String, - to: String) - : Pair, () -> Unit> { + to: String + ) : Pair, () -> Unit> { return Pair( api(connectionConfig).copyUssFile( authorizationToken = connectionConfig.authToken, @@ -154,13 +154,13 @@ class UssToUssFileMover(private val dataOpsManager: DataOpsManager) : AbstractFi * * @return target destination in String format */ - private fun computeUssDestination(operation: MoveCopyOperation) : String { + private fun computeUssDestination(operation: MoveCopyOperation): String { val destinationRootPath = (operation.destinationAttributes as RemoteUssAttributes).path val destinationNewName = operation.newName val destinationNewNameWithDelimiter = USS_DELIMITER + operation.newName // Copying or Moving USS directory return if (operation.source.isDirectory && operation.destination.isDirectory) { - destinationRootPath + if (destinationNewName != null) destinationNewNameWithDelimiter else "" + destinationRootPath + if (destinationNewName != null) destinationNewNameWithDelimiter else "" } // Copying or Moving USS file else destinationRootPath + USS_DELIMITER + (operation.newName ?: operation.sourceAttributes?.name) 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 ce03cd9d1..ddf9395fe 100644 --- a/src/main/kotlin/eu/ibagroup/formainframe/explorer/actions/GetFilePropertiesAction.kt +++ b/src/main/kotlin/eu/ibagroup/formainframe/explorer/actions/GetFilePropertiesAction.kt @@ -85,7 +85,11 @@ class GetFilePropertiesAction : AnAction() { val initFileMode = attributes.fileMode?.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) { + 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, diff --git a/src/main/kotlin/eu/ibagroup/formainframe/explorer/actions/RenameAction.kt b/src/main/kotlin/eu/ibagroup/formainframe/explorer/actions/RenameAction.kt index 3ff46c88d..b2749524b 100644 --- a/src/main/kotlin/eu/ibagroup/formainframe/explorer/actions/RenameAction.kt +++ b/src/main/kotlin/eu/ibagroup/formainframe/explorer/actions/RenameAction.kt @@ -24,16 +24,18 @@ import eu.ibagroup.formainframe.analytics.AnalyticsService import eu.ibagroup.formainframe.analytics.events.FileAction import eu.ibagroup.formainframe.analytics.events.FileEvent import eu.ibagroup.formainframe.dataops.DataOpsManager -import eu.ibagroup.formainframe.dataops.attributes.FileAttributes -import eu.ibagroup.formainframe.dataops.attributes.RemoteDatasetAttributes -import eu.ibagroup.formainframe.dataops.attributes.RemoteMemberAttributes -import eu.ibagroup.formainframe.dataops.attributes.RemoteUssAttributes +import eu.ibagroup.formainframe.dataops.attributes.* import eu.ibagroup.formainframe.dataops.content.synchronizer.checkFileForSync import eu.ibagroup.formainframe.dataops.operations.RenameOperation import eu.ibagroup.formainframe.explorer.ui.* import eu.ibagroup.formainframe.telemetry.NotificationsService +import eu.ibagroup.formainframe.v3.operations.OperationsService +import eu.ibagroup.formainframe.v3.operations.RenameOperationData import eu.ibagroup.formainframe.vfs.MFVirtualFile +typealias ConnectionConfigNew = eu.ibagroup.formainframe.v3.ConnectionConfig +typealias UssRequesterNew = eu.ibagroup.formainframe.v3.UssRequester + /** * Class which represents a "Rename" action. * The action is shown and triggered only on [UssFileNode], [UssDirNode] (not a USS mask), @@ -41,13 +43,12 @@ import eu.ibagroup.formainframe.vfs.MFVirtualFile */ class RenameAction : AnAction() { - override fun getActionUpdateThread(): ActionUpdateThread { - return ActionUpdateThread.EDT - } + override fun getActionUpdateThread() = ActionUpdateThread.EDT /** * Method to run rename operation. It passes the control to rename operation runner * @param project the current project + * @param view the file explorer view to refresh same nodes elsewhere for * @param file the virtual file to be renamed * @param type the type of the virtual file to be renamed * @param attributes remote attributes of the given virtual file @@ -58,6 +59,7 @@ class RenameAction : AnAction() { */ private fun runRenameOperation( project: Project?, + view: FileExplorerView, file: VirtualFile, type: String, attributes: FileAttributes, @@ -70,18 +72,51 @@ class RenameAction : AnAction() { cancellable = true ) { runCatching { - DataOpsManager.getService() - .performOperation( - operation = RenameOperation( - file = file, - attributes = attributes, - newName = newName - ), - progressIndicator = it + // TODO: rework + val originConnectionConfig = if (node is ExplorerUnitTreeNodeBase<*, *, *>) { + node.unit.connectionConfig + } else null + val oldRequester = if (attributes is MFRemoteFileAttributes<*, *>) { + attributes.requesters.find { it.connectionConfig == originConnectionConfig } + } else null + if (oldRequester == null || oldRequester !is UssRequester) { + DataOpsManager.getService() + .performOperation( + operation = RenameOperation( + file = file, + attributes = attributes, + newName = newName + ), + progressIndicator = it + ) + } else { + val oldConnectionConfig = oldRequester.connectionConfig + val newConnectionConfig = ConnectionConfigNew( + oldConnectionConfig.uuid, + oldConnectionConfig.name, + oldConnectionConfig.url, + oldConnectionConfig.isAllowSelfSigned, + oldConnectionConfig.zVersion, + oldConnectionConfig.owner ) + val newRequester = UssRequesterNew(newConnectionConfig) + OperationsService.getService() + .performOperation( + operationData = RenameOperationData( + file = file, + attributes = attributes, + newName = newName, + origin = newRequester + ), + progressIndicator = it + ) + } } .onSuccess { - node.parent?.cleanCacheIfPossible(cleanBatchedQuery = true) + val nodesToRefresh = view.myFsTreeStructure.findByValue(node.value) + nodesToRefresh.forEach { + it.parent?.cleanCacheIfPossible(cleanBatchedQuery = true) + } } .onFailure { NotificationsService.errorNotification(it, project) @@ -134,7 +169,7 @@ class RenameAction : AnAction() { if (checkFileForSync(e.project, file, checkDependentFiles = true)) return val dialog = RenameDialog(e.project, type, selectedNodeData, this, state) if (dialog.showAndGet()) { - runRenameOperation(e.project, file, type, attributes, dialog.state, node) + runRenameOperation(e.project, view, file, type, attributes, dialog.state, node) AnalyticsService.getService().trackAnalyticsEvent(FileEvent(attributes, FileAction.RENAME)) } } diff --git a/src/main/kotlin/eu/ibagroup/formainframe/explorer/actions/SubmitJobToolbarAction.kt b/src/main/kotlin/eu/ibagroup/formainframe/explorer/actions/SubmitJobToolbarAction.kt index c634931e9..0085be141 100644 --- a/src/main/kotlin/eu/ibagroup/formainframe/explorer/actions/SubmitJobToolbarAction.kt +++ b/src/main/kotlin/eu/ibagroup/formainframe/explorer/actions/SubmitJobToolbarAction.kt @@ -32,9 +32,7 @@ import eu.ibagroup.formainframe.utils.sendTopic */ class SubmitJobToolbarAction : AnAction() { - override fun getActionUpdateThread(): ActionUpdateThread { - return ActionUpdateThread.EDT - } + override fun getActionUpdateThread() = ActionUpdateThread.EDT /** * Submit a job on button click diff --git a/src/main/kotlin/eu/ibagroup/formainframe/explorer/ui/CommonExplorerTreeStructure.kt b/src/main/kotlin/eu/ibagroup/formainframe/explorer/ui/CommonExplorerTreeStructure.kt index b4b0fb5c9..a64ff1ea2 100644 --- a/src/main/kotlin/eu/ibagroup/formainframe/explorer/ui/CommonExplorerTreeStructure.kt +++ b/src/main/kotlin/eu/ibagroup/formainframe/explorer/ui/CommonExplorerTreeStructure.kt @@ -55,14 +55,18 @@ class CommonExplorerTreeStructure>( lock.withLock { val alreadyRegisteredNodes = valueToNodeMap .getOrPut(node.value) { LinkedList() } - .filter { it.parent != null && it.parent == node.parent } + .filter { it.path == node.path } if (alreadyRegisteredNodes.isNotEmpty()) { alreadyRegisteredNodes.forEach { alreadyRegisteredNode -> valueToNodeMap.getOrPut(node.value) { LinkedList() } - .removeIf { nodeInMap -> nodeInMap.parent == alreadyRegisteredNode.parent } + .removeIf { nodeInMap -> + nodeInMap.path == alreadyRegisteredNode.path + } node.virtualFile?.let { fileToNodeMap.getOrPut(it) { LinkedList() } - .removeIf { nodeInMap -> nodeInMap.parent == alreadyRegisteredNode.parent } + .removeIf { nodeInMap -> + nodeInMap.path == alreadyRegisteredNode.path + } } } } @@ -72,6 +76,7 @@ class CommonExplorerTreeStructure>( } } + // TODO: rework to something more stable /** * Refresh the nodes that belong to the same virtual file as the node provided with the new presentation. * Will invalidate the parent of these nodes if the parent is a [DSMaskNode] @@ -88,8 +93,8 @@ class CommonExplorerTreeStructure>( return } val nodesToRefresh = valueToNodeMap - .getOrPut(node.virtualFile) { LinkedList() } - .filter { it.parent != null && node.parent != it.parent } + .getOrPut(node.value) { LinkedList() } + .filter { it.parent != null && node.parent != null && node.parent.path != it.parent.path } if (nodesToRefresh.isNotEmpty()) { nodesToRefresh.forEach { val virtualFile = it.virtualFile 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 92a1d64cf..9363d80f3 100644 --- a/src/main/kotlin/eu/ibagroup/formainframe/explorer/ui/DSMaskNode.kt +++ b/src/main/kotlin/eu/ibagroup/formainframe/explorer/ui/DSMaskNode.kt @@ -179,6 +179,9 @@ class DSMaskNode( other as DSMaskNode + // For some reason, it does not correctly check for null value + @Suppress("SENSELESS_COMPARISON") + if (other.toString() == null) return false if (query != other.query) return false if (requestClass != other.requestClass) return false diff --git a/src/main/kotlin/eu/ibagroup/formainframe/explorer/ui/ExplorerPasteProvider.kt b/src/main/kotlin/eu/ibagroup/formainframe/explorer/ui/ExplorerPasteProvider.kt index 443758eee..e62e8b178 100644 --- a/src/main/kotlin/eu/ibagroup/formainframe/explorer/ui/ExplorerPasteProvider.kt +++ b/src/main/kotlin/eu/ibagroup/formainframe/explorer/ui/ExplorerPasteProvider.kt @@ -127,9 +127,7 @@ class ExplorerPasteProvider : PasteProvider { it.attributes?.isPastePossible ?: true } - override fun getActionUpdateThread(): ActionUpdateThread { - return ActionUpdateThread.EDT - } + override fun getActionUpdateThread() = ActionUpdateThread.EDT /** * Get nodes to refresh. Normally it would be some parent nodes that are changed during the copy/move operation. @@ -162,13 +160,13 @@ class ExplorerPasteProvider : PasteProvider { .asSequence() .map { file -> explorerView.myFsTreeStructure.findByVirtualFile(file).reversed() } .flatten() - .distinct() + .distinctBy { it.path } .toList() return if (explorerView.isCut.get()) { val sourceNodesToRefresh = sourceFilesToRefresh .map { file -> explorerView.myFsTreeStructure.findByVirtualFile(file).reversed().map { it } } .flatten() - .distinct() + .distinctBy { it.path } mutableMapOf(Pair(SOURCES, sourceNodesToRefresh), Pair(DESTINATIONS, destinationNodesToRefresh)) } else { mutableMapOf(Pair(DESTINATIONS, destinationNodesToRefresh)) diff --git a/src/main/kotlin/eu/ibagroup/formainframe/explorer/ui/ExplorerTreeView.kt b/src/main/kotlin/eu/ibagroup/formainframe/explorer/ui/ExplorerTreeView.kt index 58f9ff8c1..79c7b4988 100644 --- a/src/main/kotlin/eu/ibagroup/formainframe/explorer/ui/ExplorerTreeView.kt +++ b/src/main/kotlin/eu/ibagroup/formainframe/explorer/ui/ExplorerTreeView.kt @@ -301,7 +301,7 @@ abstract class ExplorerTreeView { - null + nodes } else -> { @@ -487,7 +487,7 @@ abstract class ExplorerTreeView>() + .toList() .forEach { it.cleanCache( recursively = it is UssDirNode, diff --git a/src/main/kotlin/eu/ibagroup/formainframe/utils/crudable/annotations/Column.kt b/src/main/kotlin/eu/ibagroup/formainframe/utils/crudable/annotations/Column.kt index 68ed51f02..db7aa1d5a 100644 --- a/src/main/kotlin/eu/ibagroup/formainframe/utils/crudable/annotations/Column.kt +++ b/src/main/kotlin/eu/ibagroup/formainframe/utils/crudable/annotations/Column.kt @@ -16,7 +16,7 @@ package eu.ibagroup.formainframe.utils.crudable.annotations import java.lang.annotation.Inherited /** - * Interface to describe the column in configuration services + * Annotation to describe a column in configuration services * @param name the name of the column * @param unique property to show that the column must be unique */ diff --git a/src/main/kotlin/eu/ibagroup/formainframe/v3/ConnectionConfig.kt b/src/main/kotlin/eu/ibagroup/formainframe/v3/ConnectionConfig.kt new file mode 100644 index 000000000..5711a211f --- /dev/null +++ b/src/main/kotlin/eu/ibagroup/formainframe/v3/ConnectionConfig.kt @@ -0,0 +1,69 @@ +/* + * 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.v3 + +import eu.ibagroup.formainframe.utils.crudable.annotations.Column +import org.zowe.kotlinsdk.annotations.ZVersion + +/** + * Connection config based class. Could be used as a basic connection config representation or as a base for other + * connection config type + * @param uuid the unique UUID of the entity + * @param name the name of the connection config + * @param url the URL for the connection config + * @param isAllowSelfSigned to indicate whether it is allowed to use self-signed certificates during a connection + * @param zVersion the version of the z/OS being used for the connection + * @param owner the actual USS user related to the USS user provided in the connection to work with in the USS part + */ +open class ConnectionConfig( + uuid: String = EMPTY_ID, + @Column var name: String = "", + @Column var url: String = "", + @Column var isAllowSelfSigned: Boolean = true, + @Column var zVersion: ZVersion = ZVersion.ZOS_2_3, + @Column var owner: String = "" +) : EntityWithUuid(uuid) { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + if (!super.equals(other)) return false + + other as ConnectionConfig + + if (name != other.name) return false + if (url != other.url) return false + if (isAllowSelfSigned != other.isAllowSelfSigned) return false + if (zVersion != other.zVersion) return false + if (owner != other.owner) return false + + return true + } + + override fun hashCode(): Int { + var result = super.hashCode() + result = 31 * result + name.hashCode() + result = 31 * result + url.hashCode() + result = 31 * result + isAllowSelfSigned.hashCode() + result = 31 * result + zVersion.hashCode() + result = 31 * result + owner.hashCode() + return result + } + + override fun toString(): String { + return "ConnectionConfig(name='$name', url='$url', isAllowSelfSigned=$isAllowSelfSigned, zVersion=$zVersion, owner=$owner)" + } + +} diff --git a/src/main/kotlin/eu/ibagroup/formainframe/v3/EntityWithUuid.kt b/src/main/kotlin/eu/ibagroup/formainframe/v3/EntityWithUuid.kt new file mode 100644 index 000000000..700d3604b --- /dev/null +++ b/src/main/kotlin/eu/ibagroup/formainframe/v3/EntityWithUuid.kt @@ -0,0 +1,43 @@ +/* + * 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.v3 + +import eu.ibagroup.formainframe.utils.crudable.annotations.Column + +/** + * Class that represents an entity with UUID + * @param uuid the UUID of the entity + */ +abstract class EntityWithUuid(@Column(unique = true) var uuid: String = EMPTY_ID) { + + companion object { + const val EMPTY_ID = "" + } + + override fun hashCode(): Int { + return uuid.hashCode() + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || javaClass != other.javaClass) return false + val that = other as EntityWithUuid + return uuid == that.uuid + } + + override fun toString(): String { + return "EntityWithUuid{uuid='$uuid'}" + } + +} diff --git a/src/main/kotlin/eu/ibagroup/formainframe/v3/Requester.kt b/src/main/kotlin/eu/ibagroup/formainframe/v3/Requester.kt new file mode 100644 index 000000000..031b140d1 --- /dev/null +++ b/src/main/kotlin/eu/ibagroup/formainframe/v3/Requester.kt @@ -0,0 +1,23 @@ +/* + * 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.v3 + +/** + * Interface to track requests origins. Represents the elements that requested a related data + * @property connectionConfig the related connection config + */ +interface Requester { + val connectionConfig: ConnectionConfigType +} diff --git a/src/main/kotlin/eu/ibagroup/formainframe/v3/UssRequester.kt b/src/main/kotlin/eu/ibagroup/formainframe/v3/UssRequester.kt new file mode 100644 index 000000000..33a055917 --- /dev/null +++ b/src/main/kotlin/eu/ibagroup/formainframe/v3/UssRequester.kt @@ -0,0 +1,20 @@ +/* + * 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.v3 + +/** Class to track USS requests origins */ +class UssRequester( + override val connectionConfig: ConnectionConfigType +) : Requester diff --git a/src/main/kotlin/eu/ibagroup/formainframe/v3/operations/OperationData.kt b/src/main/kotlin/eu/ibagroup/formainframe/v3/operations/OperationData.kt new file mode 100644 index 000000000..6353adff8 --- /dev/null +++ b/src/main/kotlin/eu/ibagroup/formainframe/v3/operations/OperationData.kt @@ -0,0 +1,28 @@ +/* + * 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.v3.operations + +import eu.ibagroup.formainframe.v3.ConnectionConfig +import eu.ibagroup.formainframe.v3.Requester + +/** + * Interface to describe an operation data + * @property resultClass the result class of the result that should be returned after an operation execution + * @property origin the exact operation requester to distinguish the source of the operation request + */ +interface OperationData { + val resultClass: Class + val origin: Requester? +} diff --git a/src/main/kotlin/eu/ibagroup/formainframe/v3/operations/OperationRunner.kt b/src/main/kotlin/eu/ibagroup/formainframe/v3/operations/OperationRunner.kt new file mode 100644 index 000000000..0a398ae47 --- /dev/null +++ b/src/main/kotlin/eu/ibagroup/formainframe/v3/operations/OperationRunner.kt @@ -0,0 +1,46 @@ +/* + * 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.v3.operations + +import com.intellij.openapi.progress.DumbProgressIndicator +import com.intellij.openapi.progress.ProgressIndicator +import eu.ibagroup.formainframe.v3.ConnectionConfig + +/** + * Base abstract class to represent operation runner + * @property operationDataClass the operation class supported by the operation runner + * @property resultClass the result class of the operation + */ +abstract class OperationRunner> { + + abstract val operationDataClass: Class> + + abstract val resultClass: Class + + /** + * Determines if an operation could be run with the provided operation class instance + * @param operationData the operation data class instance to check before the operation run + */ + abstract fun canRun(operationData: O): Boolean + + /** + * Run the operation + * @param operationData the related operation data to run the operation with the params + * @param progressIndicator the progress indicator to provide an information about the progress of the operation and + * cancel it when the progress indicator is finished + */ + abstract fun run(operationData: O, progressIndicator: ProgressIndicator = DumbProgressIndicator.INSTANCE): R + +} diff --git a/src/main/kotlin/eu/ibagroup/formainframe/v3/operations/OperationsService.kt b/src/main/kotlin/eu/ibagroup/formainframe/v3/operations/OperationsService.kt new file mode 100644 index 000000000..b48762bea --- /dev/null +++ b/src/main/kotlin/eu/ibagroup/formainframe/v3/operations/OperationsService.kt @@ -0,0 +1,89 @@ +/* + * 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.v3.operations + +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service +import com.intellij.openapi.extensions.ExtensionPointName +import com.intellij.openapi.progress.ProgressIndicator +import eu.ibagroup.formainframe.telemetry.NotificationCompatibleException +import eu.ibagroup.formainframe.telemetry.NotificationsService +import eu.ibagroup.formainframe.v3.ConnectionConfig + +/** Service to provide the endpoint which will run operations */ +@Service(Service.Level.APP) +class OperationsService { + + companion object { + private val EP = ExtensionPointName.create>("eu.ibagroup.formainframe.operationRunnerV3") + fun getService(): OperationsService = service() + } + + /** + * Find the [OperationRunner] by the provided [OperationData] class + * @param operation the class instance, inherited from [OperationData] class to search the respective operation runner by + */ + private fun > findOperationRunner( + operation: O + ): OperationRunner? { + val foundRunner = EP.extensionList + .find { it.operationDataClass == operation::class.java } + if (foundRunner == null) { + NotificationsService.errorNotification( + NotificationCompatibleException("Operation runner for operation $operation is not found") + ) + } + @Suppress("UNCHECKED_CAST") + return foundRunner as OperationRunner? + } + + // TODO: LoggerService + /** + * Perform operation for the provided operation + * @param operationData the operation data instance to run the operation with + * @param progressIndicator the progress indicator to finish the operation by + */ + fun > performOperation( + operationData: O, + progressIndicator: ProgressIndicator + ): R? { + val operationRunner = findOperationRunner(operationData) +// var startOpMessage = "Operation '${opRunner.operationClass.simpleName}' has been started" +// if (operation is Query<*, *>) { +// startOpMessage += "\nRequest params: ${operation.request}" +// } + val result = runCatching { +// operationRunner.log.info(startOpMessage) + val canRun = operationRunner?.canRun(operationData) + if (canRun == true) { + operationRunner.run(operationData, progressIndicator) + } else { + if (canRun != null) { + NotificationsService.errorNotification( + NotificationCompatibleException("The operation $operationData is not supported by the $operationRunner") + ) + } + null + } + }.onSuccess { +// opRunner.log.info("Operation '${opRunner.operationClass.simpleName}' has been completed successfully") + }.onFailure { +// opRunner.log.info("Operation '${opRunner.operationClass.simpleName}' has failed", it) + throw it + } + return result.getOrNull() + } + +} \ No newline at end of file diff --git a/src/main/kotlin/eu/ibagroup/formainframe/v3/operations/RenameOperationData.kt b/src/main/kotlin/eu/ibagroup/formainframe/v3/operations/RenameOperationData.kt new file mode 100644 index 000000000..74341e020 --- /dev/null +++ b/src/main/kotlin/eu/ibagroup/formainframe/v3/operations/RenameOperationData.kt @@ -0,0 +1,33 @@ +/* + * 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.v3.operations + +import com.intellij.openapi.vfs.VirtualFile +import eu.ibagroup.formainframe.dataops.attributes.FileAttributes +import eu.ibagroup.formainframe.v3.ConnectionConfig +import eu.ibagroup.formainframe.v3.Requester + +/** + * Class that represents a rename operation data + * @param file the virtual file to rename + * @param attributes the virtual file's attributes + * @param newName the new name to apply to the virtual file + */ +data class RenameOperationData( + val file: VirtualFile, + val attributes: FileAttributes, + val newName: String, + override val origin: Requester +) : UnitOperationData diff --git a/src/main/kotlin/eu/ibagroup/formainframe/v3/operations/RenameOperationRunner.kt b/src/main/kotlin/eu/ibagroup/formainframe/v3/operations/RenameOperationRunner.kt new file mode 100644 index 000000000..f28bd5047 --- /dev/null +++ b/src/main/kotlin/eu/ibagroup/formainframe/v3/operations/RenameOperationRunner.kt @@ -0,0 +1,186 @@ +/* + * 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.v3.operations + +import com.intellij.openapi.progress.ProgressIndicator +import eu.ibagroup.formainframe.api.api +import eu.ibagroup.formainframe.config.connect.authToken +import eu.ibagroup.formainframe.dataops.DataOpsManager +import eu.ibagroup.formainframe.dataops.attributes.RemoteDatasetAttributes +import eu.ibagroup.formainframe.dataops.attributes.RemoteMemberAttributes +import eu.ibagroup.formainframe.dataops.attributes.RemoteUssAttributes +import eu.ibagroup.formainframe.dataops.attributes.Requester +import eu.ibagroup.formainframe.dataops.exceptions.CallException +import eu.ibagroup.formainframe.utils.cancelByIndicator +import eu.ibagroup.formainframe.utils.runWriteActionInEdtAndWait +import eu.ibagroup.formainframe.v3.ConnectionConfig +import org.zowe.kotlinsdk.DataAPI +import org.zowe.kotlinsdk.FilePath +import org.zowe.kotlinsdk.MoveUssFile +import org.zowe.kotlinsdk.RenameData +import retrofit2.Call + +typealias ConnectionConfigOld = eu.ibagroup.formainframe.config.connect.ConnectionConfig +typealias UssRequesterOld = eu.ibagroup.formainframe.dataops.attributes.UssRequester + +/** [RenameOperationData] runner */ +class RenameOperationRunner : + UnitOperationRunner>() { + + override val operationDataClass = RenameOperationData::class.java + + /** + * Allow operation run only for datasets, members and USS files / folders + * @see [OperationRunner.canRun] + */ + override fun canRun(operationData: RenameOperationData): Boolean { + return with(operationData.attributes) { + this is RemoteMemberAttributes || this is RemoteDatasetAttributes || this is RemoteUssAttributes + } + } + + /** + * Run the [RenameOperationData] with the provided parameters + * @see [OperationRunner.run] + */ + override fun run( + operationData: RenameOperationData, + progressIndicator: ProgressIndicator + ) { + when (val attributes = operationData.attributes) { + is RemoteDatasetAttributes -> { + // TODO: rework entirely + // TODO: requesters.forEach - remove + attributes.requesters.forEach { + val renameOperationCallBuilder = { connectionConfig: ConnectionConfigOld -> + api(connectionConfig).renameDataset( + authorizationToken = connectionConfig.authToken, + body = RenameData( + fromDataset = RenameData.FromDataset( + oldDatasetName = attributes.name + ) + ), + toDatasetName = operationData.newName + ) + } + processRenameOperation( + operationData, + progressIndicator, + it, + renameOperationCallBuilder, + "Unable to rename the selected dataset" + ) + } + } + + is RemoteMemberAttributes -> { + // TODO: rework entirely + val parentAttributes = DataOpsManager.getService() + .tryToGetAttributes(attributes.parentFile) as RemoteDatasetAttributes + // TODO: requesters.forEach - remove + parentAttributes.requesters.forEach { + val renameOperationCallBuilder = { connectionConfig: ConnectionConfigOld -> + api(connectionConfig).renameDatasetMember( + authorizationToken = connectionConfig.authToken, + body = RenameData( + fromDataset = RenameData.FromDataset( + oldDatasetName = parentAttributes.datasetInfo.name, + oldMemberName = attributes.info.name + ) + ), + toDatasetName = parentAttributes.datasetInfo.name, + memberName = operationData.newName + ) + } + processRenameOperation( + operationData, + progressIndicator, + it, + renameOperationCallBuilder, + "Unable to rename the selected member" + ) + } + } + + is RemoteUssAttributes -> { + // TODO: rework + val newRequester = operationData.origin + val oldRequester = UssRequesterOld( + ConnectionConfigOld( + newRequester.connectionConfig.uuid, + newRequester.connectionConfig.name, + newRequester.connectionConfig.url, + newRequester.connectionConfig.isAllowSelfSigned, + newRequester.connectionConfig.zVersion, + newRequester.connectionConfig.owner + ) + ) + val parentDirPath = attributes.parentDirPath + val renameOperationCallBuilder = { connectionConfig: ConnectionConfigOld -> + api(connectionConfig).moveUssFile( + authorizationToken = connectionConfig.authToken, + body = MoveUssFile( + from = attributes.path + ), + filePath = FilePath("$parentDirPath/${operationData.newName}") + ) + } + processRenameOperation( + operationData, + progressIndicator, + oldRequester, + renameOperationCallBuilder, + "Unable to rename the selected file or directory" + ) + } + } + } + + /** + * Send the rename operation call and rename the respective elements in the virtual file system + * @param operationData the [RenameOperationData] instance + * @param progressIndicator the progress indicator to cancel the operation by on the call end + * @param requester ... + * @param renameOperationCallBuilder the operation call builder to build the call + * @param exceptionMsg the exception message to put in the [CallException] if the operation was not successful + */ + private fun processRenameOperation( + operationData: RenameOperationData, + progressIndicator: ProgressIndicator, + requester: Requester, + renameOperationCallBuilder: (ConnectionConfigOld) -> Call, + exceptionMsg: String = "Unable to rename the element" + ) { + try { + progressIndicator.checkCanceled() + val response = renameOperationCallBuilder(requester.connectionConfig) + .cancelByIndicator(progressIndicator) + .execute() + if (response.isSuccessful) { + runWriteActionInEdtAndWait { + operationData.file.rename(this, operationData.newName) + } + } else { + throw CallException(response, exceptionMsg) + } + } catch (e: Throwable) { + if (e is CallException) { + throw e + } else { + throw RuntimeException(e) + } + } + } +} diff --git a/src/main/kotlin/eu/ibagroup/formainframe/v3/operations/UnitOperationData.kt b/src/main/kotlin/eu/ibagroup/formainframe/v3/operations/UnitOperationData.kt new file mode 100644 index 000000000..2a9ca7074 --- /dev/null +++ b/src/main/kotlin/eu/ibagroup/formainframe/v3/operations/UnitOperationData.kt @@ -0,0 +1,25 @@ +/* + * 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.v3.operations + +import eu.ibagroup.formainframe.v3.ConnectionConfig + +/** + * Interface to create operations without expecting any exact result + */ +interface UnitOperationData : OperationData { + override val resultClass: Class + get() = Unit::class.java +} diff --git a/src/main/kotlin/eu/ibagroup/formainframe/v3/operations/UnitOperationRunner.kt b/src/main/kotlin/eu/ibagroup/formainframe/v3/operations/UnitOperationRunner.kt new file mode 100644 index 000000000..a40508fa1 --- /dev/null +++ b/src/main/kotlin/eu/ibagroup/formainframe/v3/operations/UnitOperationRunner.kt @@ -0,0 +1,30 @@ +/* + * 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.v3.operations + +import com.intellij.openapi.progress.ProgressIndicator +import eu.ibagroup.formainframe.v3.ConnectionConfig + +/** + * Abstract class to represent unit operation runner + * @property resultClass the result class of the operation, that is [Unit] + */ +abstract class UnitOperationRunner> : OperationRunner() { + + override val resultClass = Unit::class.java + + abstract override fun run(operationData: O, progressIndicator: ProgressIndicator) + +} diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 093aa8c50..b9d84a0a5 100755 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -102,6 +102,10 @@ Example of how to see the output:
+ + @@ -276,6 +280,8 @@ Example of how to see the output:
+ + diff --git a/src/test/kotlin/eu/ibagroup/formainframe/explorer/actions/RenameActionTestSpec.kt b/src/test/kotlin/eu/ibagroup/formainframe/explorer/actions/RenameActionTestSpec.kt index 0d3e564dc..b188e1d19 100644 --- a/src/test/kotlin/eu/ibagroup/formainframe/explorer/actions/RenameActionTestSpec.kt +++ b/src/test/kotlin/eu/ibagroup/formainframe/explorer/actions/RenameActionTestSpec.kt @@ -22,18 +22,22 @@ import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VirtualFile import eu.ibagroup.formainframe.config.ConfigService import eu.ibagroup.formainframe.config.connect.ConnectionConfig +import eu.ibagroup.formainframe.config.ws.DSMask import eu.ibagroup.formainframe.config.ws.FilesWorkingSetConfig import eu.ibagroup.formainframe.dataops.DataOpsManager import eu.ibagroup.formainframe.dataops.Operation import eu.ibagroup.formainframe.dataops.attributes.* import eu.ibagroup.formainframe.dataops.content.synchronizer.checkFileForSync import eu.ibagroup.formainframe.explorer.Explorer +import eu.ibagroup.formainframe.explorer.FilesWorkingSet import eu.ibagroup.formainframe.explorer.ui.* import eu.ibagroup.formainframe.telemetry.NotificationsService import eu.ibagroup.formainframe.testutils.WithApplicationShouldSpec import eu.ibagroup.formainframe.testutils.testServiceImpl.TestDataOpsManagerImpl import eu.ibagroup.formainframe.testutils.testServiceImpl.TestNotificationsServiceImpl import eu.ibagroup.formainframe.utils.* +import eu.ibagroup.formainframe.v3.operations.OperationsService +import eu.ibagroup.formainframe.v3.operations.RenameOperationData import eu.ibagroup.formainframe.vfs.MFVirtualFile import io.kotest.assertions.assertSoftly import io.kotest.matchers.shouldBe @@ -41,6 +45,7 @@ import io.mockk.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext +import org.zowe.kotlinsdk.annotations.ZVersion import java.util.* class RenameActionTestSpec : WithApplicationShouldSpec({ @@ -91,6 +96,9 @@ class RenameActionTestSpec : WithApplicationShouldSpec({ isEnabledAndVisible = false every { fileExplorerViewMock.mySelectedNodesData } returns listOf(selectedNodeDataMock) + every { + fileExplorerViewMock.myFsTreeStructure + } returns mockk>>() mockkStatic("eu.ibagroup.formainframe.explorer.ui.ExplorerTreeViewKt") every { anActionEventMock.getExplorerView() } returns fileExplorerViewMock @@ -112,6 +120,20 @@ class RenameActionTestSpec : WithApplicationShouldSpec({ } } + val operationsServiceMock = mockk { + every { + performOperation( + any>(), + any() + ) + } answers { + renamed = true + Unit + } + } + mockkObject(OperationsService.Companion) + every { OperationsService.getService() } returns operationsServiceMock + mockkObject(RenameDialog) every { RenameDialog["initialize"](any<() -> Unit>()) } returns Unit @@ -135,11 +157,29 @@ class RenameActionTestSpec : WithApplicationShouldSpec({ context("actionPerformed") { context("rename dataset") { - val libraryNodeMock = mockk() - every { libraryNodeMock.explorer } returns explorerMock + val connectionConfigMockk = mockk { + every { uuid } returns "testUuid" + every { name } returns "testName" + every { url } returns "testUrl" + every { isAllowSelfSigned } returns true + every { zVersion } returns ZVersion.ZOS_2_3 + every { owner } returns "testOwner" + } - val attributes = mockk() - every { attributes.datasetInfo.name } returns "dataset" + val filesWorkingSetUnitMock = mockk { + every { connectionConfig } returns connectionConfigMockk + } + + val libraryNodeMock = mockk { + every { explorer } returns explorerMock + every { unit } returns filesWorkingSetUnitMock + every { value } returns mockk() + } + + val attributes = mockk { + every { datasetInfo.name } returns "dataset" + every { requesters } returns mutableListOf(MaskedRequester(connectionConfigMockk, DSMask())) + } beforeEach { every { selectedNodeDataMock.node } returns libraryNodeMock @@ -147,6 +187,12 @@ class RenameActionTestSpec : WithApplicationShouldSpec({ every { libraryNodeMock.virtualFile } returns virtualFileMock every { libraryNodeMock.parent?.cleanCacheIfPossible(any()) } returns Unit + + every { + fileExplorerViewMock.myFsTreeStructure + } returns mockk>> { + every { findByValue(any()) } returns listOf(libraryNodeMock as ExplorerTreeNode<*, Any>) + } } should("perform rename on dataset") { @@ -195,18 +241,41 @@ class RenameActionTestSpec : WithApplicationShouldSpec({ } } context("rename dataset member") { - val fileLikeDSNodeMock = mockk() - every { fileLikeDSNodeMock.explorer } returns explorerMock - every { fileLikeDSNodeMock.virtualFile } returns virtualFileMock + val connectionConfigMockk = mockk { + every { uuid } returns "testUuid" + every { name } returns "testName" + every { url } returns "testUrl" + every { isAllowSelfSigned } returns true + every { zVersion } returns ZVersion.ZOS_2_3 + every { owner } returns "testOwner" + } - val attributes = mockk() - every { attributes.info.name } returns "member" + val filesWorkingSetUnitMock = mockk { + every { connectionConfig } returns connectionConfigMockk + } + + val fileLikeDSNodeMock = mockk { + every { explorer } returns explorerMock + every { unit } returns filesWorkingSetUnitMock + every { virtualFile } returns virtualFileMock + every { value } returns mockk() + } + + val attributes = mockk { + every { info.name } returns "member" + } beforeEach { every { selectedNodeDataMock.node } returns fileLikeDSNodeMock every { selectedNodeDataMock.attributes } returns attributes every { fileLikeDSNodeMock.parent?.cleanCacheIfPossible(any()) } returns Unit + + every { + fileExplorerViewMock.myFsTreeStructure + } returns mockk>> { + every { findByValue(any()) } returns listOf(fileLikeDSNodeMock as ExplorerTreeNode<*, Any>) + } } should("perform rename on dataset member") { @@ -231,18 +300,42 @@ class RenameActionTestSpec : WithApplicationShouldSpec({ } } context("rename USS file") { - val ussFileNodeMock = mockk() - every { ussFileNodeMock.explorer } returns explorerMock + val connectionConfigMockk = mockk { + every { uuid } returns "testUuid" + every { name } returns "testName" + every { url } returns "testUrl" + every { isAllowSelfSigned } returns true + every { zVersion } returns ZVersion.ZOS_2_3 + every { owner } returns "testOwner" + } - val attributes = mockk() - every { attributes.name } returns "ussFile" - every { attributes.isDirectory } returns false + val filesWorkingSetUnitMock = mockk { + every { connectionConfig } returns connectionConfigMockk + } + + val ussFileNodeMock = mockk { + every { explorer } returns explorerMock + every { unit } returns filesWorkingSetUnitMock + every { value } returns mockk() + } + + val attributes = mockk { + every { name } returns "ussFile" + every { isDirectory } returns false + every { requesters } returns mutableListOf(UssRequester(connectionConfigMockk)) + } beforeEach { every { selectedNodeDataMock.node } returns ussFileNodeMock every { selectedNodeDataMock.attributes } returns attributes every { ussFileNodeMock.parent?.cleanCacheIfPossible(any()) } returns Unit + + every { + fileExplorerViewMock.myFsTreeStructure + } returns mockk>> { + every { findByValue(any()) } returns listOf(ussFileNodeMock as ExplorerTreeNode<*, Any>) + } } should("perform rename on USS file") { diff --git a/src/test/kotlin/eu/ibagroup/formainframe/explorer/ui/ExplorerPasteProviderTestSpec.kt b/src/test/kotlin/eu/ibagroup/formainframe/explorer/ui/ExplorerPasteProviderTestSpec.kt index d4780a14f..a1d21c4ef 100644 --- a/src/test/kotlin/eu/ibagroup/formainframe/explorer/ui/ExplorerPasteProviderTestSpec.kt +++ b/src/test/kotlin/eu/ibagroup/formainframe/explorer/ui/ExplorerPasteProviderTestSpec.kt @@ -53,21 +53,14 @@ import eu.ibagroup.formainframe.vfs.MFVirtualFileSystem import eu.ibagroup.formainframe.vfs.MFVirtualFileSystemModel import io.kotest.assertions.assertSoftly import io.kotest.matchers.shouldBe -import io.mockk.Runs -import io.mockk.clearAllMocks -import io.mockk.clearMocks -import io.mockk.every -import io.mockk.just -import io.mockk.mockk -import io.mockk.mockkObject -import io.mockk.mockkStatic -import io.mockk.spyk +import io.mockk.* import org.zowe.kotlinsdk.Dataset import org.zowe.kotlinsdk.DatasetOrganization import java.util.* import java.util.concurrent.atomic.AtomicBoolean import javax.swing.Icon import javax.swing.tree.DefaultMutableTreeNode +import javax.swing.tree.TreePath import kotlin.reflect.KFunction import kotlin.reflect.full.declaredMemberFunctions import kotlin.reflect.jvm.isAccessible @@ -147,7 +140,9 @@ class ExplorerPasteProviderTestSpec : WithApplicationShouldSpec({ every { mockedFileExplorerView.myFsTreeStructure } returns mockk() every { mockedFileExplorerView.myStructure } returns mockk() every { nodeToRefreshSource.parent } returns nodeToRefreshSource + every { nodeToRefreshSource.path } returns mockk() every { nodeToRefreshTarget.parent } returns nodeToRefreshTarget + every { nodeToRefreshTarget.path } returns mockk() every { nodeToRefreshSource.virtualFile } returns mockedSourceVFile every { nodeToRefreshTarget.virtualFile } returns mockedDestinationVFile every { mockedSourceVFile.parent } returns sourceParentFile @@ -485,8 +480,12 @@ class ExplorerPasteProviderTestSpec : WithApplicationShouldSpec({ every { mockedFileExplorerView.myFsTreeStructure.findByVirtualFile(any() as VirtualFile) } answers { isPastePerformed = true listOf( - mockk(), - mockk() + mockk { + every { path } returns mockk() + }, + mockk { + every { path } returns mockk() + } ) } every { nodeToRefreshSource.parent } answers { @@ -530,6 +529,7 @@ class ExplorerPasteProviderTestSpec : WithApplicationShouldSpec({ every { mockedSourceFile.name } returns "test_file_source" every { mockedSourceNode.virtualFile } returns mockedSourceFile every { mockedSourceNode.parent } returns mockedSourceNodeParent + every { mockedSourceNode.path } returns mockk() every { mockedSourceFile.fileSystem } returns mockk() every { mockedSourceFile.fileSystem.model } returns mockk() every { mockedSourceFile.fileSystem.model.deleteFile(any(), any()) } just Runs @@ -559,6 +559,7 @@ class ExplorerPasteProviderTestSpec : WithApplicationShouldSpec({ every { mockedTargetFile.children } returns arrayOf(childDestinationVirtualFile) every { mockedStructureTreeModelNodeTarget.userObject } returns mockedNodeTarget every { mockedNodeTarget.virtualFile } returns mockedTargetFile + every { mockedNodeTarget.path } returns mockk() // CopyPaste node buffer val copyPasteNodeData = mockk>() @@ -654,6 +655,7 @@ class ExplorerPasteProviderTestSpec : WithApplicationShouldSpec({ every { mockedSourceFile.name } returns "test_file_source" every { mockedSourceNode.virtualFile } returns mockedSourceFile every { mockedSourceNode.parent } returns mockedSourceNodeParent + every { mockedSourceNode.path } returns mockk() every { mockedSourceFile.fileSystem } returns mockk() every { mockedSourceFile.fileSystem.model } returns mockk() every { mockedSourceFile.fileSystem.model.deleteFile(any(), any()) } just Runs @@ -683,6 +685,7 @@ class ExplorerPasteProviderTestSpec : WithApplicationShouldSpec({ every { mockedTargetFile.children } returns arrayOf(childDestinationVirtualFile) every { mockedStructureTreeModelNodeTarget.userObject } returns mockedNodeTarget every { mockedNodeTarget.virtualFile } returns mockedTargetFile + every { mockedNodeTarget.path } returns mockk() // CopyPaste node buffer val copyPasteNodeData = mockk>() @@ -775,6 +778,7 @@ class ExplorerPasteProviderTestSpec : WithApplicationShouldSpec({ val mockedSourceAttributes = mockk() every { mockedSourceNode.virtualFile } returns mockedSourceFile every { mockedSourceNode.parent } returns mockedSourceNodeParent + every { mockedSourceNode.path } returns mockk() every { mockedSourceFile.name } returns "TEST.FILE" every { mockedSourceFile.isInLocalFileSystem } returns false every { mockedSourceFile.fileSystem } returns mockk() @@ -803,6 +807,7 @@ class ExplorerPasteProviderTestSpec : WithApplicationShouldSpec({ every { mockedTargetFile.children } returns arrayOf(childDestinationVirtualFile) every { mockedStructureTreeModelNodeTarget.userObject } returns mockedNodeTarget every { mockedNodeTarget.virtualFile } returns mockedTargetFile + every { mockedNodeTarget.path } returns mockk() // CopyPaste node buffer val copyPasteNodeData = mockk>()