Skip to content

Commit 0a3c08c

Browse files
committed
refactored caching by removing FileCache completely; implemented an improved CacheFilter with LRU eviction; updated StaticFileHandler to use shared cache; updated and added tests accordingly
1 parent 7f73baf commit 0a3c08c

File tree

5 files changed

+289
-170
lines changed

5 files changed

+289
-170
lines changed

src/main/java/org/example/CacheFilter.java

Lines changed: 167 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,181 @@
11
package org.example;
22

33
import java.io.IOException;
4+
import java.util.concurrent.ConcurrentHashMap;
5+
import java.util.concurrent.atomic.AtomicLong;
46

7+
/**
8+
* Thread-safe cache filter using ConcurrentHashMap
9+
* Handles caching with LRU eviction for large files
10+
*/
511
public class CacheFilter {
6-
private final FileCache cache = new FileCache();
12+
private static final int MAX_CACHE_ENTRIES = 100;
13+
private static final long MAX_CACHE_BYTES = 50 * 1024 * 1024; // 50MB
714

15+
// Lock-free concurrent cache
16+
private final ConcurrentHashMap<String, CacheEntry> cache =
17+
new ConcurrentHashMap<>(16, 0.75f, 16); // 16 segments för concurrency
18+
19+
private final AtomicLong currentBytes = new AtomicLong(0);
20+
21+
/**
22+
* Cache entry med metadata för LRU tracking
23+
*/
24+
private static class CacheEntry {
25+
final byte[] data;
26+
final AtomicLong lastAccessTime;
27+
final AtomicLong accessCount;
28+
29+
CacheEntry(byte[] data) {
30+
this.data = data;
31+
this.lastAccessTime = new AtomicLong(System.currentTimeMillis());
32+
this.accessCount = new AtomicLong(0);
33+
}
34+
35+
void recordAccess() {
36+
accessCount.incrementAndGet();
37+
lastAccessTime.set(System.currentTimeMillis());
38+
}
39+
}
40+
41+
/**
42+
* Hämta från cache eller fetch från provider (thread-safe)
43+
*/
844
public byte[] getOrFetch(String uri, FileProvider provider) throws IOException {
9-
if (cache.contains(uri)) {
10-
System.out.println("Cache hit for: " + uri);
11-
return cache.get(uri);
45+
// Kontrollera cache (lock-free read)
46+
if (cache.containsKey(uri)) {
47+
CacheEntry entry = cache.get(uri);
48+
if (entry != null) {
49+
entry.recordAccess();
50+
System.out.println(" Cache hit for: " + uri);
51+
return entry.data;
52+
}
1253
}
54+
55+
// Cache miss - fetch från provider
56+
System.out.println(" Cache miss for: " + uri);
1357
byte[] fileBytes = provider.fetch(uri);
14-
cache.put(uri, fileBytes);
58+
59+
// Lägg till i cache
60+
addToCache(uri, fileBytes);
61+
1562
return fileBytes;
1663
}
17-
64+
65+
/**
66+
* Lägg till i cache med eviction om nödvändigt (thread-safe)
67+
*/
68+
private synchronized void addToCache(String uri, byte[] data) {
69+
// Kontrollera om vi måste evicta innan vi lägger till
70+
while (shouldEvict(data)) {
71+
evictLeastRecentlyUsed();
72+
}
73+
74+
CacheEntry newEntry = new CacheEntry(data);
75+
CacheEntry oldEntry = cache.put(uri, newEntry);
76+
77+
// Uppdatera byte-count
78+
if (oldEntry != null) {
79+
currentBytes.addAndGet(-oldEntry.data.length);
80+
}
81+
currentBytes.addAndGet(data.length);
82+
}
83+
84+
/**
85+
* Kontrollera om vi behöver evicta
86+
*/
87+
private boolean shouldEvict(byte[] newValue) {
88+
return cache.size() >= MAX_CACHE_ENTRIES ||
89+
(currentBytes.get() + newValue.length) > MAX_CACHE_BYTES;
90+
}
91+
92+
/**
93+
* Evicta minst nyligen använd entry
94+
*/
95+
private void evictLeastRecentlyUsed() {
96+
if (cache.isEmpty()) return;
97+
98+
// Hitta entry med minst senaste access
99+
String keyToRemove = cache.entrySet().stream()
100+
.min((a, b) -> Long.compare(
101+
a.getValue().lastAccessTime.get(),
102+
b.getValue().lastAccessTime.get()
103+
))
104+
.map(java.util.Map.Entry::getKey)
105+
.orElse(null);
106+
107+
if (keyToRemove != null) {
108+
CacheEntry removed = cache.remove(keyToRemove);
109+
if (removed != null) {
110+
currentBytes.addAndGet(-removed.data.length);
111+
System.out.println("✗ Evicted from cache: " + keyToRemove +
112+
" (accesses: " + removed.accessCount.get() + ")");
113+
}
114+
}
115+
}
116+
117+
// Diagnostik-metoder
118+
public int getCacheSize() {
119+
return cache.size();
120+
}
121+
122+
public long getCurrentBytes() {
123+
return currentBytes.get();
124+
}
125+
126+
public long getMaxBytes() {
127+
return MAX_CACHE_BYTES;
128+
}
129+
130+
public double getCacheUtilization() {
131+
return (double) currentBytes.get() / MAX_CACHE_BYTES * 100;
132+
}
133+
134+
public void clearCache() {
135+
cache.clear();
136+
currentBytes.set(0);
137+
}
138+
139+
public CacheStats getStats() {
140+
long totalAccesses = cache.values().stream()
141+
.mapToLong(e -> e.accessCount.get())
142+
.sum();
143+
144+
return new CacheStats(
145+
cache.size(),
146+
currentBytes.get(),
147+
MAX_CACHE_ENTRIES,
148+
MAX_CACHE_BYTES,
149+
totalAccesses
150+
);
151+
}
152+
153+
// Stats-klass
154+
public static class CacheStats {
155+
public final int entries;
156+
public final long bytes;
157+
public final int maxEntries;
158+
public final long maxBytes;
159+
public final long totalAccesses;
160+
161+
CacheStats(int entries, long bytes, int maxEntries, long maxBytes, long totalAccesses) {
162+
this.entries = entries;
163+
this.bytes = bytes;
164+
this.maxEntries = maxEntries;
165+
this.maxBytes = maxBytes;
166+
this.totalAccesses = totalAccesses;
167+
}
168+
169+
@Override
170+
public String toString() {
171+
return String.format(
172+
"CacheStats{entries=%d/%d, bytes=%d/%d, utilization=%.1f%%, accesses=%d}",
173+
entries, maxEntries, bytes, maxBytes,
174+
(double) bytes / maxBytes * 100, totalAccesses
175+
);
176+
}
177+
}
178+
18179
@FunctionalInterface
19180
public interface FileProvider {
20181
byte[] fetch(String uri) throws IOException;

src/main/java/org/example/FileCache.java

Lines changed: 0 additions & 29 deletions
This file was deleted.

src/main/java/org/example/StaticFileHandler.java

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,32 +12,32 @@
1212

1313
public class StaticFileHandler {
1414
private static final String DEFAULT_WEB_ROOT = "www";
15+
16+
// ✅ EN shared cache för alla threads
17+
private static final CacheFilter SHARED_CACHE = new CacheFilter();
18+
1519
private final String webRoot;
16-
private final CacheFilter cacheFilter;
1720

18-
// Standardkonstruktor - använder "www"
1921
public StaticFileHandler() {
2022
this(DEFAULT_WEB_ROOT);
2123
}
2224

23-
// Konstruktor som tar en anpassad webRoot-sökväg (för tester)
25+
2426
public StaticFileHandler(String webRoot) {
2527
this.webRoot = webRoot;
26-
this.cacheFilter = new CacheFilter();
2728
}
2829

2930
public void sendGetRequest(OutputStream outputStream, String uri) throws IOException {
3031
try {
31-
// Sanera URI: ta bort frågetecken, hashtaggar, ledande snedstreck och null-bytes
3232
String sanitizedUri = sanitizeUri(uri);
3333

34-
// Kontrollera för sökvägsgenomgång-attacker med Path normalisering
3534
if (isPathTraversal(sanitizedUri)) {
3635
sendErrorResponse(outputStream, 403, "Forbidden");
3736
return;
3837
}
3938

40-
byte[] fileBytes = cacheFilter.getOrFetch(sanitizedUri,
39+
// Använd shared cache istället för ny instans
40+
byte[] fileBytes = SHARED_CACHE.getOrFetch(sanitizedUri,
4141
path -> Files.readAllBytes(new File(webRoot, path).toPath())
4242
);
4343

@@ -48,7 +48,6 @@ public void sendGetRequest(OutputStream outputStream, String uri) throws IOExcep
4848
outputStream.flush();
4949

5050
} catch (IOException e) {
51-
// Hantera saknad fil och andra IO-fel
5251
try {
5352
sendErrorResponse(outputStream, 404, "Not Found");
5453
} catch (IOException ex) {
@@ -58,16 +57,10 @@ public void sendGetRequest(OutputStream outputStream, String uri) throws IOExcep
5857
}
5958

6059
private String sanitizeUri(String uri) {
61-
// Ta bort frågesträngar (?)
6260
uri = uri.split("\\?")[0];
63-
64-
// Ta bort fragment (#)
6561
uri = uri.split("#")[0];
66-
67-
// Ta bort null-bytes
6862
uri = uri.replace("\0", "");
6963

70-
// Ta bort ledande snedstreck
7164
while (uri.startsWith("/")) {
7265
uri = uri.substring(1);
7366
}
@@ -76,16 +69,12 @@ private String sanitizeUri(String uri) {
7669
}
7770

7871
private boolean isPathTraversal(String uri) {
79-
// Kontrollera för kataloggenomgång-försök
8072
try {
81-
// Normalisera sökvägen för att detektera traversal-försök
8273
Path webRootPath = Paths.get(webRoot).toRealPath();
8374
Path requestedPath = webRootPath.resolve(uri).normalize();
8475

85-
// Om den normaliserade sökvägen är utanför webRoot, är det path traversal
8676
return !requestedPath.startsWith(webRootPath);
8777
} catch (IOException e) {
88-
// Om något går fel vid normalisering, behandla det som potentiell path traversal
8978
return true;
9079
}
9180
}
@@ -99,4 +88,13 @@ private void sendErrorResponse(OutputStream outputStream, int statusCode, String
9988
outputStream.write(response.build());
10089
outputStream.flush();
10190
}
91+
92+
//Diagnostik-metod
93+
public static CacheFilter.CacheStats getCacheStats() {
94+
return SHARED_CACHE.getStats();
95+
}
96+
97+
public static void clearCache() {
98+
SHARED_CACHE.clearCache();
99+
}
102100
}

0 commit comments

Comments
 (0)