Skip to content

Commit c1884de

Browse files
Allow2 Devruvnet
andcommitted
Add storage and cache implementations
FileTokenStorage (JSON file), MemoryTokenStorage (ConcurrentHashMap), FileCache (file-per-key with TTL), MemoryCache (ConcurrentHashMap with TTL). Co-Authored-By: claude-flow <ruv@ruv.net>
1 parent 0d476be commit c1884de

4 files changed

Lines changed: 294 additions & 0 deletions

File tree

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package com.allow2.service.cache;
2+
3+
import com.allow2.service.CacheInterface;
4+
import org.json.JSONException;
5+
import org.json.JSONObject;
6+
7+
import java.io.IOException;
8+
import java.nio.charset.StandardCharsets;
9+
import java.nio.file.Files;
10+
import java.nio.file.Path;
11+
import java.nio.file.Paths;
12+
13+
/**
14+
* File-based permission cache.
15+
*
16+
* <p>Each cache entry is stored as a separate JSON file in the cache directory.
17+
* Suitable for single-server deployments.</p>
18+
*/
19+
public final class FileCache implements CacheInterface {
20+
21+
private final Path cacheDir;
22+
23+
/**
24+
* @param cacheDir Directory for cache files. Created automatically if missing.
25+
*/
26+
public FileCache(String cacheDir) {
27+
this.cacheDir = Paths.get(cacheDir);
28+
try {
29+
if (!Files.exists(this.cacheDir)) {
30+
Files.createDirectories(this.cacheDir);
31+
}
32+
} catch (IOException e) {
33+
throw new RuntimeException("Failed to create cache directory: " + cacheDir, e);
34+
}
35+
}
36+
37+
@Override
38+
public String get(String key) {
39+
Path path = keyToPath(key);
40+
41+
if (!Files.exists(path)) {
42+
return null;
43+
}
44+
45+
try {
46+
String contents = new String(Files.readAllBytes(path), StandardCharsets.UTF_8);
47+
JSONObject entry = new JSONObject(contents);
48+
49+
if (!entry.has("expiresAt") || System.currentTimeMillis() / 1000 >= entry.getLong("expiresAt")) {
50+
Files.deleteIfExists(path);
51+
return null;
52+
}
53+
54+
return entry.optString("value", null);
55+
} catch (IOException | JSONException e) {
56+
try {
57+
Files.deleteIfExists(path);
58+
} catch (IOException ignored) {
59+
}
60+
return null;
61+
}
62+
}
63+
64+
@Override
65+
public void set(String key, String value, int ttl) {
66+
Path path = keyToPath(key);
67+
68+
JSONObject entry = new JSONObject();
69+
entry.put("value", value);
70+
entry.put("expiresAt", System.currentTimeMillis() / 1000 + ttl);
71+
72+
try {
73+
Files.write(path, entry.toString().getBytes(StandardCharsets.UTF_8));
74+
} catch (IOException e) {
75+
throw new RuntimeException("Failed to write cache file: " + path, e);
76+
}
77+
}
78+
79+
@Override
80+
public void delete(String key) {
81+
Path path = keyToPath(key);
82+
try {
83+
Files.deleteIfExists(path);
84+
} catch (IOException e) {
85+
// Silently ignore delete failures
86+
}
87+
}
88+
89+
/**
90+
* Convert a cache key to a safe filesystem path.
91+
*/
92+
private Path keyToPath(String key) {
93+
String safeKey = key.replaceAll("[^a-zA-Z0-9_\\-]", "_");
94+
return cacheDir.resolve(safeKey + ".json");
95+
}
96+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package com.allow2.service.cache;
2+
3+
import com.allow2.service.CacheInterface;
4+
5+
import java.util.concurrent.ConcurrentHashMap;
6+
7+
/**
8+
* In-memory cache with TTL support using ConcurrentHashMap.
9+
*
10+
* <p>No persistence between JVM restarts. Useful for testing or when you
11+
* only need to deduplicate checks within a single process lifecycle.</p>
12+
*/
13+
public final class MemoryCache implements CacheInterface {
14+
15+
private final ConcurrentHashMap<String, CacheEntry> store = new ConcurrentHashMap<>();
16+
17+
@Override
18+
public String get(String key) {
19+
CacheEntry entry = store.get(key);
20+
if (entry == null) {
21+
return null;
22+
}
23+
if (System.currentTimeMillis() / 1000 >= entry.expiresAt) {
24+
store.remove(key);
25+
return null;
26+
}
27+
return entry.value;
28+
}
29+
30+
@Override
31+
public void set(String key, String value, int ttl) {
32+
long expiresAt = System.currentTimeMillis() / 1000 + ttl;
33+
store.put(key, new CacheEntry(value, expiresAt));
34+
}
35+
36+
@Override
37+
public void delete(String key) {
38+
store.remove(key);
39+
}
40+
41+
private static final class CacheEntry {
42+
final String value;
43+
final long expiresAt;
44+
45+
CacheEntry(String value, long expiresAt) {
46+
this.value = value;
47+
this.expiresAt = expiresAt;
48+
}
49+
}
50+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package com.allow2.service.storage;
2+
3+
import com.allow2.service.TokenStorageInterface;
4+
import com.allow2.service.models.OAuthTokens;
5+
import org.json.JSONObject;
6+
7+
import java.io.IOException;
8+
import java.nio.charset.StandardCharsets;
9+
import java.nio.file.Files;
10+
import java.nio.file.Path;
11+
import java.nio.file.Paths;
12+
import java.nio.file.attribute.PosixFilePermission;
13+
import java.util.EnumSet;
14+
15+
/**
16+
* File-based JSON token storage.
17+
*
18+
* <p>Stores all tokens in a single JSON file. Suitable for development
19+
* and single-server deployments. Not recommended for high-concurrency
20+
* production use.</p>
21+
*/
22+
public final class FileTokenStorage implements TokenStorageInterface {
23+
24+
private final Path filePath;
25+
private JSONObject data;
26+
27+
/**
28+
* @param filePath Path to the JSON storage file.
29+
*/
30+
public FileTokenStorage(String filePath) {
31+
this.filePath = Paths.get(filePath);
32+
}
33+
34+
@Override
35+
public synchronized void store(String userId, OAuthTokens tokens) {
36+
JSONObject all = loadAll();
37+
JSONObject tokenObj = new JSONObject();
38+
tokenObj.put("access_token", tokens.getAccessToken());
39+
tokenObj.put("refresh_token", tokens.getRefreshToken());
40+
tokenObj.put("expires_at", tokens.getExpiresAt());
41+
all.put(userId, tokenObj);
42+
saveAll(all);
43+
}
44+
45+
@Override
46+
public synchronized OAuthTokens retrieve(String userId) {
47+
JSONObject all = loadAll();
48+
if (!all.has(userId)) {
49+
return null;
50+
}
51+
return OAuthTokens.fromJsonObject(all.getJSONObject(userId));
52+
}
53+
54+
@Override
55+
public synchronized void delete(String userId) {
56+
JSONObject all = loadAll();
57+
all.remove(userId);
58+
saveAll(all);
59+
}
60+
61+
@Override
62+
public synchronized boolean exists(String userId) {
63+
JSONObject all = loadAll();
64+
return all.has(userId);
65+
}
66+
67+
private JSONObject loadAll() {
68+
if (data != null) {
69+
return data;
70+
}
71+
72+
if (!Files.exists(filePath)) {
73+
data = new JSONObject();
74+
return data;
75+
}
76+
77+
try {
78+
String contents = new String(Files.readAllBytes(filePath), StandardCharsets.UTF_8);
79+
data = new JSONObject(contents);
80+
} catch (IOException e) {
81+
data = new JSONObject();
82+
}
83+
84+
return data;
85+
}
86+
87+
private void saveAll(JSONObject allData) {
88+
this.data = allData;
89+
90+
try {
91+
Path dir = filePath.getParent();
92+
if (dir != null && !Files.exists(dir)) {
93+
Files.createDirectories(dir);
94+
}
95+
96+
Files.write(filePath, allData.toString(2).getBytes(StandardCharsets.UTF_8));
97+
98+
// Try to set file permissions to owner-only (may fail on non-POSIX systems)
99+
try {
100+
Files.setPosixFilePermissions(filePath, EnumSet.of(
101+
PosixFilePermission.OWNER_READ,
102+
PosixFilePermission.OWNER_WRITE
103+
));
104+
} catch (UnsupportedOperationException e) {
105+
// Non-POSIX filesystem, skip permission setting
106+
}
107+
} catch (IOException e) {
108+
throw new RuntimeException("Failed to save token storage file: " + filePath, e);
109+
}
110+
}
111+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.allow2.service.storage;
2+
3+
import com.allow2.service.TokenStorageInterface;
4+
import com.allow2.service.models.OAuthTokens;
5+
6+
import java.util.concurrent.ConcurrentHashMap;
7+
8+
/**
9+
* In-memory token storage using ConcurrentHashMap.
10+
*
11+
* <p>No persistence between JVM restarts. Useful for testing or
12+
* when you only need tokens for the lifetime of the process.</p>
13+
*/
14+
public final class MemoryTokenStorage implements TokenStorageInterface {
15+
16+
private final ConcurrentHashMap<String, OAuthTokens> store = new ConcurrentHashMap<>();
17+
18+
@Override
19+
public void store(String userId, OAuthTokens tokens) {
20+
store.put(userId, tokens);
21+
}
22+
23+
@Override
24+
public OAuthTokens retrieve(String userId) {
25+
return store.get(userId);
26+
}
27+
28+
@Override
29+
public void delete(String userId) {
30+
store.remove(userId);
31+
}
32+
33+
@Override
34+
public boolean exists(String userId) {
35+
return store.containsKey(userId);
36+
}
37+
}

0 commit comments

Comments
 (0)