feature/18-add-existing-files-cache #26
|
@ -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
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -32,5 +32,5 @@ class ResourceFileService(
|
|||
FilePath("classpath:${path}")
|
||||
|
||||
val FilePath.resource: Resource
|
||||
get() = resourceLoader.getResource(this.path)
|
||||
get() = resourceLoader.getResource(this.value)
|
||||
}
|
||||
|
|
|
@ -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) =
|
||||
|
|
|
@ -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,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<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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue