diff --git a/src/main/kotlin/eu/ibagroup/formainframe/config/settings/ui/SettingsConfigurable.kt b/src/main/kotlin/eu/ibagroup/formainframe/config/settings/ui/SettingsConfigurable.kt index 46bdf319..deddd9b6 100644 --- a/src/main/kotlin/eu/ibagroup/formainframe/config/settings/ui/SettingsConfigurable.kt +++ b/src/main/kotlin/eu/ibagroup/formainframe/config/settings/ui/SettingsConfigurable.kt @@ -17,7 +17,9 @@ package eu.ibagroup.formainframe.config.settings.ui import com.intellij.ide.BrowserUtil import com.intellij.openapi.observable.util.whenTextChanged import com.intellij.openapi.options.BoundSearchableConfigurable +import com.intellij.openapi.progress.runModalTask import com.intellij.openapi.ui.DialogPanel +import com.intellij.openapi.ui.Messages import com.intellij.ui.dsl.builder.bindIntText import com.intellij.ui.dsl.builder.bindSelected import com.intellij.ui.dsl.builder.panel @@ -25,6 +27,7 @@ import eu.ibagroup.formainframe.analytics.AnalyticsService import eu.ibagroup.formainframe.analytics.PolicyProvider import eu.ibagroup.formainframe.analytics.ui.AnalyticsPolicyDialog import eu.ibagroup.formainframe.config.ConfigService +import eu.ibagroup.formainframe.dataops.DataOpsManager import eu.ibagroup.formainframe.rateus.RateUsNotification import eu.ibagroup.formainframe.utils.validateBatchSize import java.util.concurrent.atomic.AtomicBoolean @@ -87,6 +90,22 @@ class SettingsConfigurable : BoundSearchableConfigurable("Settings", "mainframe" res.component.addItemListener { isAutoSyncEnabled.set(res.component.isSelected) } } } + row { + button("Clear File Cache") { + var cacheCleared = false + runModalTask("Cache Clearing", cancellable = false) { + cacheCleared = DataOpsManager.getService().clearFileCache() + } + if (cacheCleared) { + Messages.showInfoMessage( + "The file cache has been successfully cleared.", + "Cache Cleared", + ) + } + }.applyToComponent { + toolTipText = "Clear the local contents of files downloaded from the remote system. All related files opened in the editor will be closed" + } + } } group("Rate Us") { row { diff --git a/src/main/kotlin/eu/ibagroup/formainframe/dataops/DataOpsManager.kt b/src/main/kotlin/eu/ibagroup/formainframe/dataops/DataOpsManager.kt index 52559424..aec2896f 100644 --- a/src/main/kotlin/eu/ibagroup/formainframe/dataops/DataOpsManager.kt +++ b/src/main/kotlin/eu/ibagroup/formainframe/dataops/DataOpsManager.kt @@ -67,6 +67,8 @@ interface DataOpsManager : Disposable { */ fun getContentSynchronizer(file: VirtualFile): ContentSynchronizer? + fun clearFileCache(): Boolean + fun getMFContentAdapter(file: VirtualFile): MFContentAdapter fun getNameResolver(source: VirtualFile, destination: VirtualFile): CopyPasteNameResolver diff --git a/src/main/kotlin/eu/ibagroup/formainframe/dataops/DataOpsManagerImpl.kt b/src/main/kotlin/eu/ibagroup/formainframe/dataops/DataOpsManagerImpl.kt index d5e100bb..90d5a1e1 100644 --- a/src/main/kotlin/eu/ibagroup/formainframe/dataops/DataOpsManagerImpl.kt +++ b/src/main/kotlin/eu/ibagroup/formainframe/dataops/DataOpsManagerImpl.kt @@ -17,7 +17,9 @@ package eu.ibagroup.formainframe.dataops import com.intellij.execution.ui.ConsoleView import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.components.ComponentManager +import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.project.ProjectManager import com.intellij.openapi.util.Disposer import com.intellij.openapi.vfs.VirtualFile import eu.ibagroup.formainframe.dataops.attributes.AttributesService @@ -25,6 +27,7 @@ import eu.ibagroup.formainframe.dataops.attributes.FileAttributes import eu.ibagroup.formainframe.dataops.content.adapters.DefaultContentAdapter import eu.ibagroup.formainframe.dataops.content.adapters.MFContentAdapter import eu.ibagroup.formainframe.dataops.content.synchronizer.ContentSynchronizer +import eu.ibagroup.formainframe.dataops.content.synchronizer.checkForSync import eu.ibagroup.formainframe.dataops.fetch.FileFetchProvider import eu.ibagroup.formainframe.dataops.log.AbstractMFLoggerBase import eu.ibagroup.formainframe.dataops.log.LogFetcher @@ -36,6 +39,8 @@ import eu.ibagroup.formainframe.dataops.operations.mover.names.DefaultNameResolv import eu.ibagroup.formainframe.utils.associateListedBy import eu.ibagroup.formainframe.utils.findAnyNullable import eu.ibagroup.formainframe.utils.log +import eu.ibagroup.formainframe.utils.runInEdtAndWait +import eu.ibagroup.formainframe.vfs.MFVirtualFile /** * Data operation manager implementation class. @@ -170,6 +175,32 @@ class DataOpsManagerImpl : DataOpsManager { return contentSynchronizers.firstOrNull { it.accepts(file) } } + /** + * Closes all [MFVirtualFile] files opened in the editor and clears the cache of all registered content synchronizers. + * @return true if the file cache is cleared and false otherwise. + */ + override fun clearFileCache(): Boolean { + ProjectManager.getInstance().openProjects.forEach { project -> + val fileEditorManager = FileEditorManager.getInstance(project) + runInEdtAndWait { + fileEditorManager.openFiles.forEach { + if (it is MFVirtualFile) { + fileEditorManager.closeFile(it) + } + } + } + } + var syncInProgress = false + runInEdtAndWait { + syncInProgress = checkForSync() + } + if (!syncInProgress) { + contentSynchronizers.forEach { it.clearFileCache() } + return true + } + return false + } + /** * Returns instance of content adapter to mainframe * @param file object that represents file/folder on mainframe diff --git a/src/main/kotlin/eu/ibagroup/formainframe/dataops/content/service/SyncProcessService.kt b/src/main/kotlin/eu/ibagroup/formainframe/dataops/content/service/SyncProcessService.kt index 583ce9d2..d770a1f0 100644 --- a/src/main/kotlin/eu/ibagroup/formainframe/dataops/content/service/SyncProcessService.kt +++ b/src/main/kotlin/eu/ibagroup/formainframe/dataops/content/service/SyncProcessService.kt @@ -55,4 +55,10 @@ interface SyncProcessService { */ fun areDependentFilesSyncingNow(file: VirtualFile): Boolean + /** + * Check if any file is currently synchronized. + * @return true if any file is syncing now or false otherwise. + */ + fun isAnyFileSyncingNow(): Boolean + } diff --git a/src/main/kotlin/eu/ibagroup/formainframe/dataops/content/service/SyncProcessServiceImpl.kt b/src/main/kotlin/eu/ibagroup/formainframe/dataops/content/service/SyncProcessServiceImpl.kt index ab2917b2..b3c0d1a0 100644 --- a/src/main/kotlin/eu/ibagroup/formainframe/dataops/content/service/SyncProcessServiceImpl.kt +++ b/src/main/kotlin/eu/ibagroup/formainframe/dataops/content/service/SyncProcessServiceImpl.kt @@ -56,4 +56,11 @@ class SyncProcessServiceImpl: SyncProcessService { } } + /** + * Base implementation of [SyncProcessService.isAnyFileSyncingNow] method. + */ + override fun isAnyFileSyncingNow(): Boolean { + return fileToProgressIndicatorMap.values.any { it.isRunning } + } + } diff --git a/src/main/kotlin/eu/ibagroup/formainframe/dataops/content/synchronizer/ContentSynchronizer.kt b/src/main/kotlin/eu/ibagroup/formainframe/dataops/content/synchronizer/ContentSynchronizer.kt index 16396806..0e5edb71 100644 --- a/src/main/kotlin/eu/ibagroup/formainframe/dataops/content/synchronizer/ContentSynchronizer.kt +++ b/src/main/kotlin/eu/ibagroup/formainframe/dataops/content/synchronizer/ContentSynchronizer.kt @@ -79,4 +79,9 @@ interface ContentSynchronizer { */ fun markAsNotNeededForSync(syncProvider: SyncProvider) + /** + * Clears all file records from the content storage and other related values. + */ + fun clearFileCache() + } diff --git a/src/main/kotlin/eu/ibagroup/formainframe/dataops/content/synchronizer/RemoteAttributedContentSynchronizer.kt b/src/main/kotlin/eu/ibagroup/formainframe/dataops/content/synchronizer/RemoteAttributedContentSynchronizer.kt index 4e9258bf..7ee9d820 100644 --- a/src/main/kotlin/eu/ibagroup/formainframe/dataops/content/synchronizer/RemoteAttributedContentSynchronizer.kt +++ b/src/main/kotlin/eu/ibagroup/formainframe/dataops/content/synchronizer/RemoteAttributedContentSynchronizer.kt @@ -92,7 +92,6 @@ abstract class RemoteAttributedContentSynchronizer val attributesService by lazy { dataOpsManager.getAttributesService(attributesClass, vFileClass) } private val successfulStatesStorage by lazy { ContentStorage(SUCCESSFUL_CONTENT_STORAGE_NAME_PREFIX + entityName) } private val handlerToStorageIdMap = ConcurrentHashMap() - private val idToBinaryFileMap = ConcurrentHashMap() private val fetchedAtLeastOnce = ConcurrentHashMap.newKeySet() private val needToUpload = ConcurrentHashMap.newKeySet() @@ -228,4 +227,16 @@ abstract class RemoteAttributedContentSynchronizer override fun markAsNotNeededForSync(syncProvider: SyncProvider) { needToUpload.remove(syncProvider) } + + /** + * Base implementation of [ContentSynchronizer.clearFileCache] method for each content synchronizer. + */ + override fun clearFileCache() { + handlerToStorageIdMap.values.forEach { + successfulStatesStorage.deleteRecord(it) + } + handlerToStorageIdMap.clear() + fetchedAtLeastOnce.clear() + needToUpload.clear() + } } diff --git a/src/main/kotlin/eu/ibagroup/formainframe/dataops/content/synchronizer/syncUtils.kt b/src/main/kotlin/eu/ibagroup/formainframe/dataops/content/synchronizer/syncUtils.kt index 195c9969..547046e1 100644 --- a/src/main/kotlin/eu/ibagroup/formainframe/dataops/content/synchronizer/syncUtils.kt +++ b/src/main/kotlin/eu/ibagroup/formainframe/dataops/content/synchronizer/syncUtils.kt @@ -79,3 +79,17 @@ fun checkFileForSync( true } else false } + +/** + * Check if any file is syncing now and show the warning dialog if so. + * @param project project to show dialog. + * @return true if any file is syncing now and false otherwise. + */ +fun checkForSync(project: Project? = null): Boolean { + val message = "You can't perform this action because some file is currently being synchronized" + val title = "Synchronization Is In Progress" + return if (SyncProcessService.getService().isAnyFileSyncingNow()) { + Messages.showWarningDialog(project, message, title) + true + } else false +} diff --git a/src/test/kotlin/eu/ibagroup/formainframe/dataops/ContentTestSpec.kt b/src/test/kotlin/eu/ibagroup/formainframe/dataops/ContentTestSpec.kt index b506537b..2ad72a3c 100644 --- a/src/test/kotlin/eu/ibagroup/formainframe/dataops/ContentTestSpec.kt +++ b/src/test/kotlin/eu/ibagroup/formainframe/dataops/ContentTestSpec.kt @@ -20,7 +20,9 @@ import com.intellij.openapi.ui.Messages import com.intellij.openapi.vfs.VfsUtilCore import com.intellij.openapi.vfs.VirtualFile import eu.ibagroup.formainframe.dataops.content.service.SyncProcessService +import eu.ibagroup.formainframe.dataops.content.service.SyncProcessServiceImpl import eu.ibagroup.formainframe.dataops.content.synchronizer.checkFileForSync +import eu.ibagroup.formainframe.dataops.content.synchronizer.checkForSync import eu.ibagroup.formainframe.testutils.WithApplicationShouldSpec import eu.ibagroup.formainframe.testutils.testServiceImpl.TestSyncProcessServiceImpl import io.kotest.assertions.assertSoftly @@ -115,6 +117,28 @@ class ContentTestSpec : WithApplicationShouldSpec({ wasWarningShown shouldBe false } } + // syncUtils.checkForSync + should("check that any file is syncing") { + syncProcessService.testInstance = object : TestSyncProcessServiceImpl() { + override fun isAnyFileSyncingNow(): Boolean { + return true + } + } + + val result = checkForSync(mockk()) + assertSoftly { + result shouldBe true + wasWarningShown shouldBe true + } + } + should("check that no file is syncing") { + val result = checkForSync() + + assertSoftly { + result shouldBe false + wasWarningShown shouldBe false + } + } // SyncAction.actionPerformed should("synchronize the file with the remote file") {} // MemberContentSynchronizer.fetchRemoteContentBytes @@ -133,56 +157,59 @@ class ContentTestSpec : WithApplicationShouldSpec({ should("adapt content for the dataset from mainframe with variable print length") {} } context("dataops module: content/service") { + val syncProcessService = SyncProcessServiceImpl() val virtualFileMock = mockk() val progressIndicatorMock = mockk() beforeEach { + syncProcessService.startFileSync(virtualFileMock, progressIndicatorMock) + val isAncestorRef: (VirtualFile, VirtualFile, Boolean) -> Boolean = VfsUtilCore::isAncestor mockkStatic(isAncestorRef as KFunction<*>) } afterEach { + syncProcessService.stopFileSync(virtualFileMock) + unmockkAll() } // SyncProcessServiceImpl.isFileSyncingNow - // TODO: rewrite to the service usage -// should("file is syncing now") { -// every { progressIndicatorMock.isRunning } returns true -// -// val result = SyncProcessService.getService().isFileSyncingNow(virtualFileMock) -// -// assertSoftly { -// result shouldBe true -// } -// } + should("file is syncing now") { + every { progressIndicatorMock.isRunning } returns true + + val result = syncProcessService.isFileSyncingNow(virtualFileMock) + + assertSoftly { + result shouldBe true + } + } should("file is not syncing now") { every { progressIndicatorMock.isRunning } returns false - val result = SyncProcessService.getService().isFileSyncingNow(virtualFileMock) + val result = syncProcessService.isFileSyncingNow(virtualFileMock) assertSoftly { result shouldBe false } } // SyncProcessServiceImpl.areDependentFilesSyncingNow - // TODO: rewrite to the service usage -// should("dependent files are syncing now") { -// every { progressIndicatorMock.isRunning } returns true -// every { VfsUtilCore.isAncestor(virtualFileMock, any(), true) } returns true -// -// val result = SyncProcessService.getService().areDependentFilesSyncingNow(virtualFileMock) -// -// assertSoftly { -// result shouldBe true -// } -// } + should("dependent files are syncing now") { + every { progressIndicatorMock.isRunning } returns true + every { VfsUtilCore.isAncestor(virtualFileMock, any(), true) } returns true + + val result = syncProcessService.areDependentFilesSyncingNow(virtualFileMock) + + assertSoftly { + result shouldBe true + } + } should("dependent files are not syncing now") { every { progressIndicatorMock.isRunning } returns true every { VfsUtilCore.isAncestor(virtualFileMock, any(), true) } returns false - val result = SyncProcessService.getService().areDependentFilesSyncingNow(virtualFileMock) + val result = syncProcessService.areDependentFilesSyncingNow(virtualFileMock) assertSoftly { result shouldBe false @@ -191,7 +218,26 @@ class ContentTestSpec : WithApplicationShouldSpec({ should("dependent files are not syncing now because no sync is running") { every { progressIndicatorMock.isRunning } returns false - val result = SyncProcessService.getService().areDependentFilesSyncingNow(virtualFileMock) + val result = syncProcessService.areDependentFilesSyncingNow(virtualFileMock) + + assertSoftly { + result shouldBe false + } + } + // SyncProcessServiceImpl.isAnyFileSyncingNow + should("any file is syncing now") { + every { progressIndicatorMock.isRunning } returns true + + val result = syncProcessService.isAnyFileSyncingNow() + + assertSoftly { + result shouldBe true + } + } + should("no file is syncing now") { + every { progressIndicatorMock.isRunning } returns false + + val result = syncProcessService.isAnyFileSyncingNow() assertSoftly { result shouldBe false diff --git a/src/test/kotlin/eu/ibagroup/formainframe/dataops/DataOpsManagerImplTestSpec.kt b/src/test/kotlin/eu/ibagroup/formainframe/dataops/DataOpsManagerImplTestSpec.kt new file mode 100644 index 00000000..e43499fe --- /dev/null +++ b/src/test/kotlin/eu/ibagroup/formainframe/dataops/DataOpsManagerImplTestSpec.kt @@ -0,0 +1,93 @@ +/* + * 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.dataops + +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.project.ProjectManager +import eu.ibagroup.formainframe.dataops.content.synchronizer.checkForSync +import eu.ibagroup.formainframe.testutils.WithApplicationShouldSpec +import eu.ibagroup.formainframe.vfs.MFVirtualFile +import io.kotest.assertions.assertSoftly +import io.kotest.matchers.shouldBe +import io.mockk.* + +class DataOpsManagerImplTestSpec : WithApplicationShouldSpec({ + + afterSpec { + clearAllMocks() + } + + context("DataOpsMangerImpl test spec") { + val dataOpsManager = DataOpsManagerImpl() + + var fileClosed = false + + val projectManagerMock = mockk() + every { projectManagerMock.openProjects } returns arrayOf(mockk()) + + val fileEditorManagerMock = mockk() + + beforeEach { + mockkStatic(ProjectManager::getInstance) + every { ProjectManager.getInstance() } returns projectManagerMock + + every { fileEditorManagerMock.openFiles } returns arrayOf(mockk()) + fileClosed = false + every { fileEditorManagerMock.closeFile(any()) } answers { + fileClosed = true + } + + mockkStatic(FileEditorManager::getInstance) + every { FileEditorManager.getInstance(any()) } returns fileEditorManagerMock + + mockkStatic(::checkForSync) + every { checkForSync(any()) } returns false + } + + afterEach { + unmockkAll() + } + + // DataOpsMangerImpl.clearFileCache + should("clear the file cache with closing the files in the editor") { + val result = dataOpsManager.clearFileCache() + + assertSoftly { + result shouldBe true + fileClosed shouldBe true + } + } + should("clear the file cache without closing the files in the editor") { + every { fileEditorManagerMock.openFiles } returns arrayOf(mockk()) + + val result = dataOpsManager.clearFileCache() + + assertSoftly { + result shouldBe true + fileClosed shouldBe false + } + } + should("do not clear the file cache because the files are synchronized") { + every { checkForSync(any()) } returns true + + val result = dataOpsManager.clearFileCache() + + assertSoftly { + result shouldBe false + fileClosed shouldBe true + } + } + } +}) \ No newline at end of file diff --git a/src/test/kotlin/eu/ibagroup/formainframe/testutils/testServiceImpl/TestDataOpsManagerImpl.kt b/src/test/kotlin/eu/ibagroup/formainframe/testutils/testServiceImpl/TestDataOpsManagerImpl.kt index 4d6289aa..7ab7e8c0 100644 --- a/src/test/kotlin/eu/ibagroup/formainframe/testutils/testServiceImpl/TestDataOpsManagerImpl.kt +++ b/src/test/kotlin/eu/ibagroup/formainframe/testutils/testServiceImpl/TestDataOpsManagerImpl.kt @@ -77,6 +77,10 @@ open class TestDataOpsManagerImpl : DataOpsManager { return contentSynchronizerMock } + override fun clearFileCache(): Boolean { + TODO("Not yet implemented") + } + override fun getMFContentAdapter(file: VirtualFile): MFContentAdapter { TODO("Not yet implemented") } @@ -140,6 +144,10 @@ open class TestDataOpsManagerImpl : DataOpsManager { return this.testInstance.getContentSynchronizer(file) } + override fun clearFileCache(): Boolean { + return this.testInstance.clearFileCache() + } + override fun getMFContentAdapter(file: VirtualFile): MFContentAdapter { return this.testInstance.getMFContentAdapter(file) } diff --git a/src/test/kotlin/eu/ibagroup/formainframe/testutils/testServiceImpl/TestSyncProcessServiceImpl.kt b/src/test/kotlin/eu/ibagroup/formainframe/testutils/testServiceImpl/TestSyncProcessServiceImpl.kt index 81f98dc1..c3ab6eaf 100644 --- a/src/test/kotlin/eu/ibagroup/formainframe/testutils/testServiceImpl/TestSyncProcessServiceImpl.kt +++ b/src/test/kotlin/eu/ibagroup/formainframe/testutils/testServiceImpl/TestSyncProcessServiceImpl.kt @@ -38,6 +38,10 @@ open class TestSyncProcessServiceImpl : SyncProcessService { return false } + override fun isAnyFileSyncingNow(): Boolean { + return false + } + } override fun startFileSync(file: VirtualFile, progressIndicator: ProgressIndicator) { @@ -56,4 +60,8 @@ open class TestSyncProcessServiceImpl : SyncProcessService { return testInstance.areDependentFilesSyncingNow(file) } + override fun isAnyFileSyncingNow(): Boolean { + return testInstance.isAnyFileSyncingNow() + } + } \ No newline at end of file