#18 Move file cache to interface
continuous-integration/drone/push Build is passing Details

This commit is contained in:
FyloZ 2022-01-24 23:09:02 -05:00
parent e6b1ba3b45
commit c42fc26a92
Signed by: william
GPG Key ID: 835378AE9AF4AE97
6 changed files with 126 additions and 91 deletions

View File

@ -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<String, CachedFileSystemItem>()
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
override fun directoryExists(path: FilePath) =
exists(path) && this[path] is CachedDirectory
override fun fileExists(path: FilePath) =
exists(path) && this[path] is CachedFile
override fun setExists(path: FilePath, exists: Boolean) {
if (path !in this) {
load(path)
}
fun exists(filePath: FilePath) =
filePath in this && cache[filePath.path]!!.exists
fun directoryExists(filePath: FilePath) =
exists(filePath) && this[filePath] is CachedDirectory
fun fileExists(filePath: FilePath) =
exists(filePath) && this[filePath] is CachedFile
fun setExists(filePath: FilePath, exists: Boolean = true) {
if (filePath !in this) {
load(filePath)
this[path] = this[path]!!.clone(exists)
logger.debug("Updated FileCache state: ${path.value} exists -> $exists")
}
this[filePath] = this[filePath]!!.clone(exists)
logger.debug("Updated FileCache state: ${filePath.path} exists -> $exists")
override fun load(path: FilePath) =
with(JavaFile(path.value).toFileSystemItem()) {
cache[path.value] = this
logger.debug("Loaded file at ${path.value} into FileCache")
}
fun setDoesNotExists(filePath: FilePath) =
setExists(filePath, false)
fun load(filePath: FilePath) =
with(JavaFile(filePath.path).toFileSystemItem()) {
cache[filePath.path] = this
logger.debug("Loaded file at ${filePath.path} 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
}

View File

@ -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<CachedFile> =
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)

View File

@ -32,5 +32,5 @@ class ResourceFileService(
FilePath("classpath:${path}")
val FilePath.resource: Resource
get() = resourceLoader.getResource(this.path)
get() = resourceLoader.getResource(this.value)
}

View File

@ -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 <T> withFileAt(fullPath: FilePath, block: File.() -> T) =

View File

@ -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<FileCache> {
every { setExists(any(), any()) } just runs
}
private val fileService = spyk(FileServiceImpl(fileCacheMock, creProperties))
private val mockFile = mockk<File> {
every { file } returns mockk()
every { exists() } returns true
@ -43,12 +47,10 @@ 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()
}
}
private fun whenFileNotCached(test: () -> Unit) {
whenFileCached(false, test)
@ -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<FileWriteException> {
@ -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)
}
}

View File

@ -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<Resource>()
every { resourceLoader.getResource(filePath.path) } returns resource
every { resourceLoader.getResource(filePath.value) } returns resource
with(service) {
val found = filePath.resource