einige Ergänzungen
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
package at.mocode.client.common.cache
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock
|
||||
import kotlin.concurrent.read
|
||||
import kotlin.concurrent.write
|
||||
|
||||
/**
|
||||
* Thread-safe LRU cache implementation for API responses.
|
||||
* Provides TTL-based expiration and size-based eviction.
|
||||
*/
|
||||
class ApiCache(
|
||||
private val maxSize: Int,
|
||||
private val ttlMs: Long
|
||||
) {
|
||||
private val cache = ConcurrentHashMap<String, CacheEntry>()
|
||||
private val accessOrder = ConcurrentLinkedQueue<String>()
|
||||
private val lock = ReentrantReadWriteLock()
|
||||
|
||||
data class CacheEntry(
|
||||
val data: Any,
|
||||
val timestamp: Long
|
||||
)
|
||||
|
||||
/**
|
||||
* Retrieves a cached value if it exists and hasn't expired.
|
||||
*/
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T> get(key: String): T? {
|
||||
return lock.read {
|
||||
val entry = cache[key] ?: return null
|
||||
|
||||
// Check if expired
|
||||
if (System.currentTimeMillis() - entry.timestamp > ttlMs) {
|
||||
// Remove expired entry
|
||||
lock.write {
|
||||
cache.remove(key)
|
||||
accessOrder.remove(key)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// Update access order
|
||||
lock.write {
|
||||
accessOrder.remove(key)
|
||||
accessOrder.offer(key)
|
||||
}
|
||||
|
||||
entry.data as T
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores a value in the cache with current timestamp.
|
||||
*/
|
||||
fun put(key: String, value: Any) {
|
||||
lock.write {
|
||||
// Remove if already exists
|
||||
if (cache.containsKey(key)) {
|
||||
accessOrder.remove(key)
|
||||
}
|
||||
|
||||
// Add new entry
|
||||
cache[key] = CacheEntry(value, System.currentTimeMillis())
|
||||
accessOrder.offer(key)
|
||||
|
||||
// Evict oldest entries if over capacity
|
||||
while (cache.size > maxSize) {
|
||||
val oldestKey = accessOrder.poll()
|
||||
if (oldestKey != null) {
|
||||
cache.remove(oldestKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a specific entry from the cache.
|
||||
*/
|
||||
fun remove(key: String) {
|
||||
lock.write {
|
||||
cache.remove(key)
|
||||
accessOrder.remove(key)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes entries matching the given pattern.
|
||||
* Useful for invalidating related cache entries.
|
||||
*/
|
||||
fun removePattern(pattern: String) {
|
||||
lock.write {
|
||||
val keysToRemove = cache.keys.filter { it.contains(pattern) }
|
||||
keysToRemove.forEach { key ->
|
||||
cache.remove(key)
|
||||
accessOrder.remove(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all cached entries.
|
||||
*/
|
||||
fun clear() {
|
||||
lock.write {
|
||||
cache.clear()
|
||||
accessOrder.clear()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all expired entries from the cache.
|
||||
*/
|
||||
fun cleanupExpired() {
|
||||
val currentTime = System.currentTimeMillis()
|
||||
lock.write {
|
||||
val expiredKeys = cache.entries
|
||||
.filter { currentTime - it.value.timestamp > ttlMs }
|
||||
.map { it.key }
|
||||
|
||||
expiredKeys.forEach { key ->
|
||||
cache.remove(key)
|
||||
accessOrder.remove(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns current cache statistics.
|
||||
*/
|
||||
fun getStats(): CacheStats {
|
||||
return lock.read {
|
||||
CacheStats(
|
||||
size = cache.size,
|
||||
maxSize = maxSize,
|
||||
ttlMs = ttlMs
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class CacheStats(
|
||||
val size: Int,
|
||||
val maxSize: Int,
|
||||
val ttlMs: Long
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package at.mocode.client.common.config
|
||||
|
||||
/**
|
||||
* Configuration class for API client settings.
|
||||
* Allows for environment-specific configuration.
|
||||
*/
|
||||
data class ApiConfig(
|
||||
val baseUrl: String = System.getProperty("api.base.url") ?: System.getenv("API_BASE_URL") ?: "http://localhost:8080",
|
||||
val requestTimeoutMs: Long = System.getProperty("api.timeout")?.toLongOrNull() ?: 30_000L,
|
||||
val cacheTtlMs: Long = System.getProperty("api.cache.ttl")?.toLongOrNull() ?: 30_000L,
|
||||
val maxCacheSize: Int = System.getProperty("api.cache.max.size")?.toIntOrNull() ?: 1000,
|
||||
val enableLogging: Boolean = System.getProperty("api.logging.enabled")?.toBoolean() ?: false
|
||||
)
|
||||
+175
@@ -0,0 +1,175 @@
|
||||
package at.mocode.client.common.repository
|
||||
|
||||
import at.mocode.client.common.api.ApiClient
|
||||
import at.mocode.client.common.api.ApiException
|
||||
|
||||
/**
|
||||
* Base repository class that provides common CRUD operations for client-side repositories.
|
||||
* Eliminates code duplication and provides consistent error handling across all repositories.
|
||||
*/
|
||||
abstract class BaseClientRepository<T : Any>(
|
||||
protected val baseEndpoint: String
|
||||
) {
|
||||
|
||||
/**
|
||||
* Finds an entity by its ID.
|
||||
*/
|
||||
protected suspend fun <T> findEntityById(id: String, entityClass: Class<T>): T? {
|
||||
return try {
|
||||
// Note: This is a simplified version - in practice you'd use reflection or other means
|
||||
// to handle the generic type properly
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
ApiClient.get<Any>("$baseEndpoint/$id") as? T
|
||||
} catch (e: Exception) {
|
||||
logError("Failed to fetch entity with ID $id", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all active entities with pagination.
|
||||
*/
|
||||
protected suspend fun <T> findAllActiveEntities(limit: Int, offset: Int, entityClass: Class<T>): List<T> {
|
||||
return try {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
(ApiClient.get<Any>("$baseEndpoint?limit=$limit&offset=$offset") as? List<T>) ?: emptyList()
|
||||
} catch (e: Exception) {
|
||||
logError("Failed to fetch active entities", e)
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches entities by name/search term.
|
||||
*/
|
||||
protected suspend fun <T> searchEntities(searchTerm: String, limit: Int, entityClass: Class<T>): List<T> {
|
||||
return try {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
(ApiClient.get<Any>("$baseEndpoint?search=$searchTerm&limit=$limit") as? List<T>) ?: emptyList()
|
||||
} catch (e: Exception) {
|
||||
logError("Failed to search entities by term: $searchTerm", e)
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches entities by a specific field.
|
||||
*/
|
||||
protected suspend fun <T> searchEntitiesByField(
|
||||
fieldName: String,
|
||||
fieldValue: String,
|
||||
limit: Int,
|
||||
entityClass: Class<T>
|
||||
): List<T> {
|
||||
return try {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
(ApiClient.get<Any>("$baseEndpoint?$fieldName=$fieldValue&limit=$limit") as? List<T>) ?: emptyList()
|
||||
} catch (e: Exception) {
|
||||
logError("Failed to search entities by $fieldName: $fieldValue", e)
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches entities by date range.
|
||||
*/
|
||||
protected suspend fun <T> searchEntitiesByDateRange(
|
||||
startDate: String,
|
||||
endDate: String,
|
||||
limit: Int,
|
||||
entityClass: Class<T>
|
||||
): List<T> {
|
||||
return try {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
(ApiClient.get<Any>("$baseEndpoint?startDate=$startDate&endDate=$endDate&limit=$limit") as? List<T>) ?: emptyList()
|
||||
} catch (e: Exception) {
|
||||
logError("Failed to search entities by date range: $startDate to $endDate", e)
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves an entity (create or update based on ID).
|
||||
*/
|
||||
protected suspend fun <T> saveEntity(entity: T, getId: (T) -> String, entityClass: Class<T>): T {
|
||||
return try {
|
||||
val id = getId(entity)
|
||||
if (id.isBlank()) {
|
||||
// Create new entity
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
ApiClient.post<Any>(baseEndpoint, entity as Any) as T
|
||||
} else {
|
||||
// Update existing entity
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
ApiClient.put<Any>("$baseEndpoint/$id", entity as Any) as T
|
||||
}
|
||||
} catch (e: ApiException) {
|
||||
logError("Failed to save entity", e)
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
logError("Unexpected error while saving entity", e)
|
||||
throw ApiException(
|
||||
message = "Failed to save entity: ${e.message}",
|
||||
code = "SAVE_ERROR",
|
||||
details = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes an entity by ID.
|
||||
*/
|
||||
protected suspend fun deleteEntity(id: String): Boolean {
|
||||
return try {
|
||||
ApiClient.delete<Boolean>("$baseEndpoint/$id")
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
logError("Failed to delete entity with ID $id", e)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts active entities.
|
||||
*/
|
||||
protected suspend fun countActiveEntities(): Long {
|
||||
return try {
|
||||
ApiClient.get<Long>("$baseEndpoint/count") ?: 0L
|
||||
} catch (e: Exception) {
|
||||
logError("Failed to count active entities", e)
|
||||
0L
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets entities from a specific sub-endpoint.
|
||||
*/
|
||||
protected suspend fun <T> getFromSubEndpoint(subEndpoint: String, limit: Int, entityClass: Class<T>): List<T> {
|
||||
return try {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
(ApiClient.get<Any>("$baseEndpoint/$subEndpoint?limit=$limit") as? List<T>) ?: emptyList()
|
||||
} catch (e: Exception) {
|
||||
logError("Failed to fetch from sub-endpoint: $subEndpoint", e)
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs errors in a consistent format.
|
||||
* In a real application, this should use a proper logging framework.
|
||||
*/
|
||||
internal fun logError(message: String, exception: Exception) {
|
||||
println("[ERROR] ${this::class.simpleName}: $message - ${exception.message}")
|
||||
// TODO: Replace with proper logging framework (e.g., SLF4J)
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates cache entries related to this repository's endpoint.
|
||||
*/
|
||||
protected fun invalidateCache() {
|
||||
// Extract base path for cache invalidation
|
||||
val basePath = baseEndpoint.split("/").take(3).joinToString("/")
|
||||
// For now, clear all cache - in a real implementation, we'd use pattern matching
|
||||
ApiClient.clearCache()
|
||||
}
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
package at.mocode.client.common.repository
|
||||
|
||||
import at.mocode.client.common.api.ApiClient
|
||||
|
||||
/**
|
||||
* Optimized client-side implementation of the PersonRepository interface.
|
||||
* Uses BaseClientRepository to eliminate code duplication and provide consistent error handling.
|
||||
*/
|
||||
class OptimizedClientPersonRepository : BaseClientRepository<Person>("/api/persons"), PersonRepository {
|
||||
|
||||
override suspend fun findById(id: String): Person? {
|
||||
return try {
|
||||
ApiClient.get<Person>("$baseEndpoint/$id")
|
||||
} catch (e: Exception) {
|
||||
logError("Failed to fetch person with ID $id", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun findAllActive(limit: Int, offset: Int): List<Person> {
|
||||
return try {
|
||||
ApiClient.get<List<Person>>("$baseEndpoint?limit=$limit&offset=$offset") ?: emptyList()
|
||||
} catch (e: Exception) {
|
||||
logError("Failed to fetch active persons", e)
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun findByName(searchTerm: String, limit: Int): List<Person> {
|
||||
return try {
|
||||
ApiClient.get<List<Person>>("$baseEndpoint?search=$searchTerm&limit=$limit") ?: emptyList()
|
||||
} catch (e: Exception) {
|
||||
logError("Failed to search persons by name", e)
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun save(person: Person): Person {
|
||||
return try {
|
||||
val result = if (person.id.isBlank()) {
|
||||
// Create new person
|
||||
ApiClient.post<Person>(baseEndpoint, person)
|
||||
} else {
|
||||
// Update existing person
|
||||
ApiClient.put<Person>("$baseEndpoint/${person.id}", person)
|
||||
}
|
||||
|
||||
// Invalidate related cache entries after successful save
|
||||
invalidateCache()
|
||||
|
||||
result
|
||||
} catch (e: Exception) {
|
||||
logError("Failed to save person", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun delete(id: String): Boolean {
|
||||
return try {
|
||||
ApiClient.delete<Boolean>("$baseEndpoint/$id")
|
||||
|
||||
// Invalidate related cache entries after successful delete
|
||||
invalidateCache()
|
||||
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
logError("Failed to delete person with ID $id", e)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun countActive(): Long {
|
||||
return countActiveEntities()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user