diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/files/FileCache.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/files/FileCache.kt index 4ee0260..6bd2ea9 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/files/FileCache.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/files/FileCache.kt @@ -4,95 +4,129 @@ import dev.fyloz.colorrecipesexplorer.JavaFile import dev.fyloz.colorrecipesexplorer.utils.File import dev.fyloz.colorrecipesexplorer.utils.FilePath import mu.KotlinLogging +import org.springframework.stereotype.Component -object FileCache { +interface FileCache { + /** Checks if the cache contains the given [path]. */ + operator fun contains(path: FilePath): Boolean + + /** Gets the cached file system item at the given [path]. */ + operator fun get(path: FilePath): CachedFileSystemItem? + + /** Gets the cached directory at the given [path]. */ + fun getDirectory(path: FilePath): CachedDirectory? + + /** Gets the cached file at the given [path]. */ + fun getFile(path: FilePath): CachedFile? + + /** Checks if the cached file system item at the given [path] exists. */ + fun exists(path: FilePath): Boolean + + /** Checks if the cached directory at the given [path] exists. */ + fun directoryExists(path: FilePath): Boolean + + /** Checks if the cached file at the given [path] exists. */ + fun fileExists(path: FilePath): Boolean + + /** Sets the file system item at the given [path] as existing or not. Loads the item in the cache if not already present. */ + fun setExists(path: FilePath, exists: Boolean = true) + + /** Loads the file system item at the given [path] into the cache. */ + fun load(path: FilePath) + + /** Adds the file system item at the given [itemPath] to the cached directory at the given [directoryPath]. */ + fun addItemToDirectory(directoryPath: FilePath, itemPath: FilePath) + + /** Removes the file system item at the given [itemPath] from the cached directory at the given [directoryPath]. */ + fun removeItemFromDirectory(directoryPath: FilePath, itemPath: FilePath) +} + +@Component +class DefaultFileCache : FileCache { private val logger = KotlinLogging.logger {} private val cache = hashMapOf() - operator fun contains(filePath: FilePath) = - filePath.path in cache + override operator fun contains(path: FilePath) = + path.value in cache - operator fun get(filePath: FilePath) = - cache[filePath.path] + override operator fun get(path: FilePath) = + cache[path.value] - fun getDirectory(filePath: FilePath) = - if (directoryExists(filePath)) { - this[filePath] as CachedDirectory + private operator fun set(path: FilePath, item: CachedFileSystemItem) { + cache[path.value] = item + } + + override fun getDirectory(path: FilePath) = + if (directoryExists(path)) { + this[path] as CachedDirectory } else { null } - fun getFile(filePath: FilePath) = - if (fileExists(filePath)) { - this[filePath] as CachedFile + override fun getFile(path: FilePath) = + if (fileExists(path)) { + this[path] as CachedFile } else { null } - private operator fun set(filePath: FilePath, item: CachedFileSystemItem) { - cache[filePath.path] = item - } + override fun exists(path: FilePath) = + path in this && cache[path.value]!!.exists - fun exists(filePath: FilePath) = - filePath in this && cache[filePath.path]!!.exists + override fun directoryExists(path: FilePath) = + exists(path) && this[path] is CachedDirectory - fun directoryExists(filePath: FilePath) = - exists(filePath) && this[filePath] is CachedDirectory + override fun fileExists(path: FilePath) = + exists(path) && this[path] is CachedFile - fun fileExists(filePath: FilePath) = - exists(filePath) && this[filePath] is CachedFile - - fun setExists(filePath: FilePath, exists: Boolean = true) { - if (filePath !in this) { - load(filePath) + override fun setExists(path: FilePath, exists: Boolean) { + if (path !in this) { + load(path) } - this[filePath] = this[filePath]!!.clone(exists) - logger.debug("Updated FileCache state: ${filePath.path} exists -> $exists") + this[path] = this[path]!!.clone(exists) + logger.debug("Updated FileCache state: ${path.value} exists -> $exists") } - fun setDoesNotExists(filePath: FilePath) = - setExists(filePath, false) + override fun load(path: FilePath) = + with(JavaFile(path.value).toFileSystemItem()) { + cache[path.value] = this - fun load(filePath: FilePath) = - with(JavaFile(filePath.path).toFileSystemItem()) { - cache[filePath.path] = this - - logger.debug("Loaded file at ${filePath.path} into FileCache") + logger.debug("Loaded file at ${path.value} into FileCache") } - fun addContent(filePath: FilePath, childFilePath: FilePath) { - val directory = prepareDirectory(filePath) ?: return + override fun addItemToDirectory(directoryPath: FilePath, itemPath: FilePath) { + val directory = prepareDirectory(directoryPath) ?: return val updatedContent = setOf( *directory.content.toTypedArray(), - JavaFile(childFilePath.path).toFileSystemItem() + JavaFile(itemPath.value).toFileSystemItem() ) - this[filePath] = directory.copy(content = updatedContent) - logger.debug("Added child ${childFilePath.path} to ${filePath.path} in FileCache") + this[directoryPath] = directory.copy(content = updatedContent) + logger.debug("Added child ${itemPath.value} to ${directoryPath.value} in FileCache") } - fun removeContent(filePath: FilePath, childFilePath: FilePath) { - val directory = prepareDirectory(filePath) ?: return + override fun removeItemFromDirectory(directoryPath: FilePath, itemPath: FilePath) { + val directory = prepareDirectory(directoryPath) ?: return val updatedContent = directory.content - .filter { it.path.path != childFilePath.path } + .filter { it.path.value != itemPath.value } .toSet() - this[filePath] = directory.copy(content = updatedContent) - logger.debug("Removed child ${childFilePath.path} from ${filePath.path} in FileCache") + this[directoryPath] = directory.copy(content = updatedContent) + logger.debug("Removed child ${itemPath.value} from ${directoryPath.value} in FileCache") } - private fun prepareDirectory(filePath: FilePath): CachedDirectory? { - if (!directoryExists(filePath)) { - logger.warn("Cannot add child to ${filePath.path} because it is not in the cache") + private fun prepareDirectory(path: FilePath): CachedDirectory? { + if (!directoryExists(path)) { + logger.warn("Cannot add child to ${path.value} because it is not in the cache") return null } - val directory = getDirectory(filePath) + val directory = getDirectory(path) if (directory == null) { - logger.warn("Cannot add child to ${filePath.path} because it is not a directory") + logger.warn("Cannot add child to ${path.value} because it is not a directory") return null } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/files/FileService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/files/FileService.kt index aa5abd2..b2615b2 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/files/FileService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/files/FileService.kt @@ -57,18 +57,19 @@ interface WriteableFileService : FileService { @Service class FileServiceImpl( + private val fileCache: FileCache, private val creProperties: CreProperties ) : WriteableFileService { private val logger = KotlinLogging.logger {} override fun exists(path: String): Boolean { val fullPath = fullPath(path) - return if (fullPath in FileCache) { - FileCache.exists(fullPath) + return if (fullPath in fileCache) { + fileCache.exists(fullPath) } else { withFileAt(fullPath) { (this.exists() && this.isFile).also { - FileCache.setExists(fullPath, it) + fileCache.setExists(fullPath, it) } } } @@ -87,11 +88,11 @@ class FileServiceImpl( override fun listDirectoryFiles(path: String): Collection = with(fullPath(path)) { - if (this !in FileCache) { - FileCache.load(this) + if (this !in fileCache) { + fileCache.load(this) } - (FileCache.getDirectory(this) ?: return setOf()) + (fileCache.getDirectory(this) ?: return setOf()) .contentFiles } @@ -101,9 +102,9 @@ class FileServiceImpl( try { withFileAt(fullPath) { this.create() - FileCache.setExists(fullPath) + fileCache.setExists(fullPath) - logger.info("Created file at '${fullPath.path}'") + logger.info("Created file at '${fullPath.value}'") } } catch (ex: IOException) { FileCreateException(path).logAndThrow(ex, logger) @@ -124,7 +125,7 @@ class FileServiceImpl( } override fun writeToDirectory(data: MultipartFile, path: String, parentPath: String, overwrite: Boolean) { - FileCache.addContent(fullPath(parentPath), fullPath(path)) + fileCache.addItemToDirectory(fullPath(parentPath), fullPath(path)) write(data, path, overwrite) } @@ -135,9 +136,9 @@ class FileServiceImpl( if (!exists(path)) throw FileNotFoundException(path) this.delete() - FileCache.setDoesNotExists(fullPath) + fileCache.setExists(fullPath, false) - logger.info("Deleted file at '${fullPath.path}'") + logger.info("Deleted file at '${fullPath.value}'") } } catch (ex: IOException) { FileDeleteException(path).logAndThrow(ex, logger) @@ -145,7 +146,7 @@ class FileServiceImpl( } override fun deleteFromDirectory(path: String, parentPath: String) { - FileCache.removeContent(fullPath(parentPath), fullPath(path)) + fileCache.removeItemFromDirectory(fullPath(parentPath), fullPath(path)) delete(path) } @@ -170,7 +171,7 @@ class FileServiceImpl( withFileAt(fullPath) { this.op() - logger.info("Wrote data to file at '${fullPath.path}'") + logger.info("Wrote data to file at '${fullPath.value}'") } } catch (ex: IOException) { FileWriteException(path).logAndThrow(ex, logger) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/files/ResourceFileService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/files/ResourceFileService.kt index 5c4d116..1502233 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/files/ResourceFileService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/files/ResourceFileService.kt @@ -32,5 +32,5 @@ class ResourceFileService( FilePath("classpath:${path}") val FilePath.resource: Resource - get() = resourceLoader.getResource(this.path) + get() = resourceLoader.getResource(this.value) } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/utils/Files.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/utils/Files.kt index d336667..eea31dd 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/utils/Files.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/utils/Files.kt @@ -41,12 +41,12 @@ class File(val file: JavaFile) { File(JavaFile(path)) fun from(path: FilePath) = - from(path.path) + from(path.value) } } // TODO: Move to value class when mocking them with mockk works -class FilePath(val path: String) +class FilePath(val value: String) /** Runs the given [block] in the context of a file with the given [fullPath]. */ fun withFileAt(fullPath: FilePath, block: File.() -> T) = diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/files/FileServiceTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/files/FileServiceTest.kt index 44a6990..0f02ee4 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/files/FileServiceTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/files/FileServiceTest.kt @@ -22,7 +22,11 @@ private val mockFilePathPath = Path.of(mockFilePath) private val mockFileData = byteArrayOf(0x1, 0x8, 0xa, 0xf) class FileServiceTest { - private val fileService = spyk(FileServiceImpl(creProperties)) + private val fileCacheMock = mockk { + every { setExists(any(), any()) } just runs + } + private val fileService = spyk(FileServiceImpl(fileCacheMock, creProperties)) + private val mockFile = mockk { every { file } returns mockk() every { exists() } returns true @@ -43,11 +47,9 @@ class FileServiceTest { } private fun whenFileCached(cached: Boolean = true, test: () -> Unit) { - mockkObject(FileCache) { - every { FileCache.contains(any()) } returns cached + every { fileCacheMock.contains(any()) } returns cached - test() - } + test() } private fun whenFileNotCached(test: () -> Unit) { @@ -106,34 +108,34 @@ class FileServiceTest { @Test fun `exists() returns true when the file at the given path is cached as existing`() { whenFileCached { - every { FileCache.exists(any()) } returns true + every { fileCacheMock.exists(any()) } returns true assertTrue { fileService.exists(mockFilePath) } verify { - FileCache.contains(any()) - FileCache.exists(any()) + fileCacheMock.contains(any()) + fileCacheMock.exists(any()) mockFile wasNot called } - confirmVerified(FileCache, mockFile) + confirmVerified(fileCacheMock, mockFile) } } @Test fun `exists() returns false when the file at the given path is cached as not existing`() { whenFileCached { - every { FileCache.exists(any()) } returns false + every { fileCacheMock.exists(any()) } returns false assertFalse { fileService.exists(mockFilePath) } verify { - FileCache.contains(any()) - FileCache.exists(any()) + fileCacheMock.contains(any()) + fileCacheMock.exists(any()) mockFile wasNot called } - confirmVerified(FileCache, mockFile) + confirmVerified(fileCacheMock, mockFile) } } @@ -179,17 +181,16 @@ class FileServiceTest { whenMockFilePathExists(false) { whenFileNotCached { mockkStatic(File::create) { - every { mockFile.create() } just Runs - every { FileCache.setExists(any()) } just Runs + every { mockFile.create() } just runs fileService.create(mockFilePath) verify { mockFile.create() - FileCache.setExists(any()) + fileCacheMock.setExists(any()) } - confirmVerified(mockFile, FileCache) + confirmVerified(mockFile, fileCacheMock) } } } @@ -223,8 +224,8 @@ class FileServiceTest { @Test fun `write() creates and writes the given MultipartFile to the file at the given path`() { whenMockFilePathExists(false) { - every { fileService.create(mockFilePath) } just Runs - every { mockMultipartFile.transferTo(mockFilePathPath) } just Runs + every { fileService.create(mockFilePath) } just runs + every { mockMultipartFile.transferTo(mockFilePathPath) } just runs fileService.write(mockMultipartFile, mockFilePath, false) @@ -247,7 +248,7 @@ class FileServiceTest { @Test fun `write() writes the given MultipartFile to an existing file when overwrite is enabled`() { whenMockFilePathExists { - every { mockMultipartFile.transferTo(mockFilePathPath) } just Runs + every { mockMultipartFile.transferTo(mockFilePathPath) } just runs fileService.write(mockMultipartFile, mockFilePath, true) @@ -260,7 +261,7 @@ class FileServiceTest { @Test fun `write() throws FileWriteException when writing the given file throws an IOException`() { whenMockFilePathExists(false) { - every { fileService.create(mockFilePath) } just Runs + every { fileService.create(mockFilePath) } just runs every { mockMultipartFile.transferTo(mockFilePathPath) } throws IOException() with(assertThrows { @@ -278,16 +279,15 @@ class FileServiceTest { whenMockFilePathExists { whenFileCached { every { mockFile.delete() } returns true - every { FileCache.setDoesNotExists(any()) } just Runs fileService.delete(mockFilePath) verify { mockFile.delete() - FileCache.setDoesNotExists(any()) + fileCacheMock.setExists(any(), false) } - confirmVerified(mockFile, FileCache) + confirmVerified(mockFile, fileCacheMock) } } } @@ -319,7 +319,7 @@ class FileServiceTest { with(fileService) { val fullFilePath = fullPath(mockFilePath) - assertEquals("${creProperties.dataDirectory}/$mockFilePath", fullFilePath.path) + assertEquals("${creProperties.dataDirectory}/$mockFilePath", fullFilePath.value) } } diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/files/ResourceFileServiceTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/files/ResourceFileServiceTest.kt index 55bcac2..ac806df 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/files/ResourceFileServiceTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/files/ResourceFileServiceTest.kt @@ -94,7 +94,7 @@ class ResourceFileServiceTest { val found = service.fullPath(path) - assertEquals(expectedPath, found.path) + assertEquals(expectedPath, found.value) } @Test @@ -102,7 +102,7 @@ class ResourceFileServiceTest { val filePath = FilePath("classpath:unit_test_path") val resource = mockk() - every { resourceLoader.getResource(filePath.path) } returns resource + every { resourceLoader.getResource(filePath.value) } returns resource with(service) { val found = filePath.resource