einige Ergänzungen

This commit is contained in:
2025-07-25 23:16:16 +02:00
parent 4c382e64a5
commit 7e0b56a247
70 changed files with 7795 additions and 1894 deletions
@@ -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
)
@@ -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()
}
}
@@ -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()
}
}