SearchResultCache.java
package com.university.bookstore.search;
import java.util.ArrayList;
import java.util.Deque;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import com.university.bookstore.model.Material;
/**
* LRU (Least Recently Used) cache for search results.
* Evicts least recently used items when the cache reaches its maximum size.
*
* <p>This implementation provides O(1) average time complexity for get and put operations
* by using a HashMap for O(1) lookups and a Deque for O(1) access order tracking.</p>
*
* @author Navid Mohaghegh
* @version 3.0
* @since 2024-09-15
*/
public class SearchResultCache {
private final int maxSize;
private final Map<String, CacheEntry> cache;
private final Deque<String> accessOrder;
/**
* Creates a new search result cache with the specified maximum size.
*
* @param maxSize the maximum number of entries the cache can hold
* @throws IllegalArgumentException if maxSize is not positive
*/
public SearchResultCache(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("Cache size must be positive: " + maxSize);
}
this.maxSize = maxSize;
this.cache = new HashMap<>();
this.accessOrder = new LinkedList<>();
}
/**
* Retrieves cached search results for the given key.
* Updates the access order to mark this entry as recently used.
*
* @param key the cache key
* @return Optional containing the cached results if found
*/
public Optional<List<Material>> get(String key) {
if (key == null) {
return Optional.empty();
}
CacheEntry entry = cache.get(key);
if (entry != null) {
// Move to end (most recently used)
accessOrder.remove(key);
accessOrder.addLast(key);
return Optional.of(new ArrayList<>(entry.results));
}
return Optional.empty();
}
/**
* Stores search results in the cache with the given key.
* Evicts the least recently used entry if the cache is full.
*
* @param key the cache key
* @param results the search results to cache
* @throws IllegalArgumentException if key is null
*/
public void put(String key, List<Material> results) {
if (key == null) {
throw new IllegalArgumentException("Cache key cannot be null");
}
if (results == null) {
throw new IllegalArgumentException("Search results cannot be null");
}
// If cache is full, remove least recently used entry
if (cache.size() >= maxSize && !cache.containsKey(key)) {
evictLRU();
}
// Store the new entry
cache.put(key, new CacheEntry(results));
// Update access order
accessOrder.remove(key); // Remove if already exists
accessOrder.addLast(key);
}
/**
* Checks if the cache contains results for the given key.
*
* @param key the cache key
* @return true if the key exists in the cache
*/
public boolean containsKey(String key) {
return key != null && cache.containsKey(key);
}
/**
* Removes the entry with the given key from the cache.
*
* @param key the cache key to remove
* @return true if an entry was removed, false if key not found
*/
public boolean remove(String key) {
if (key == null) {
return false;
}
CacheEntry removed = cache.remove(key);
accessOrder.remove(key);
return removed != null;
}
/**
* Clears all entries from the cache.
*/
public void clear() {
cache.clear();
accessOrder.clear();
}
/**
* Gets the current number of entries in the cache.
*
* @return the cache size
*/
public int size() {
return cache.size();
}
/**
* Gets the maximum number of entries the cache can hold.
*
* @return the maximum cache size
*/
public int getMaxSize() {
return maxSize;
}
/**
* Checks if the cache is empty.
*
* @return true if no entries are cached
*/
public boolean isEmpty() {
return cache.isEmpty();
}
/**
* Checks if the cache is full.
*
* @return true if the cache has reached its maximum size
*/
public boolean isFull() {
return cache.size() >= maxSize;
}
/**
* Gets cache statistics including hit ratio and average age.
*
* @return cache statistics
*/
public CacheStats getStats() {
long totalRequests = 0;
long totalHits = 0;
long totalAge = 0;
long currentTime = System.currentTimeMillis();
for (CacheEntry entry : cache.values()) {
totalRequests += entry.accessCount;
totalHits += entry.hitCount;
totalAge += (currentTime - entry.timestamp);
}
double hitRatio = totalRequests > 0 ? (double) totalHits / totalRequests : 0.0;
double averageAge = cache.size() > 0 ? (double) totalAge / cache.size() : 0.0;
return new CacheStats(
cache.size(),
maxSize,
hitRatio,
averageAge,
totalRequests,
totalHits
);
}
/**
* Evicts the least recently used entry from the cache.
*/
private void evictLRU() {
if (!accessOrder.isEmpty()) {
String lruKey = accessOrder.removeFirst();
cache.remove(lruKey);
}
}
/**
* Internal class representing a cache entry.
*/
private static class CacheEntry {
final List<Material> results;
final long timestamp;
long accessCount;
long hitCount;
CacheEntry(List<Material> results) {
this.results = new ArrayList<>(results);
this.timestamp = System.currentTimeMillis();
this.accessCount = 0;
this.hitCount = 0;
}
}
/**
* Statistics class for cache performance monitoring.
*/
public static class CacheStats {
private final int currentSize;
private final int maxSize;
private final double hitRatio;
private final double averageAge;
private final long totalRequests;
private final long totalHits;
public CacheStats(int currentSize, int maxSize, double hitRatio,
double averageAge, long totalRequests, long totalHits) {
this.currentSize = currentSize;
this.maxSize = maxSize;
this.hitRatio = hitRatio;
this.averageAge = averageAge;
this.totalRequests = totalRequests;
this.totalHits = totalHits;
}
public int getCurrentSize() { return currentSize; }
public int getMaxSize() { return maxSize; }
public double getHitRatio() { return hitRatio; }
public double getAverageAge() { return averageAge; }
public long getTotalRequests() { return totalRequests; }
public long getTotalHits() { return totalHits; }
@Override
public String toString() {
return String.format("CacheStats[Size=%d/%d, HitRatio=%.2f%%, AvgAge=%.0fms, Requests=%d, Hits=%d]",
currentSize, maxSize, hitRatio * 100, averageAge, totalRequests, totalHits);
}
}
@Override
public String toString() {
return String.format("SearchResultCache[Size=%d/%d, HitRatio=%.2f%%]",
size(), maxSize, getStats().getHitRatio() * 100);
}
}