diff --git a/net/src/main/java/org/xbib/net/Store.java b/net/src/main/java/org/xbib/net/Store.java index 6eb5eb7..5991821 100644 --- a/net/src/main/java/org/xbib/net/Store.java +++ b/net/src/main/java/org/xbib/net/Store.java @@ -1,6 +1,7 @@ package org.xbib.net; import java.io.IOException; +import java.util.Map; import java.util.function.Consumer; public interface Store { @@ -11,11 +12,19 @@ public interface Store { S read(String key) throws IOException; - void readAll(String key, Consumer consumer) throws IOException; + void readAll(String prefix, Consumer consumer) throws IOException; void write(String key, S s) throws IOException; + void write(String prefix, String key, S s) throws IOException; + + void update(String key, Map map) throws IOException; + + void update(String prefix, String key, Map map) throws IOException; + void remove(String key) throws IOException; void purge(long expiredAfterSeconds) throws IOException; + + void destroy() throws IOException; } diff --git a/net/src/main/java/org/xbib/net/util/JsonFileStore.java b/net/src/main/java/org/xbib/net/util/JsonFileStore.java index df243e7..1ade189 100644 --- a/net/src/main/java/org/xbib/net/util/JsonFileStore.java +++ b/net/src/main/java/org/xbib/net/util/JsonFileStore.java @@ -7,9 +7,12 @@ import org.xbib.net.PercentEncoders; import java.io.IOException; import java.io.Writer; import java.nio.charset.StandardCharsets; +import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.attribute.FileTime; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.BasicFileAttributes; import java.time.Duration; import java.time.Instant; import java.util.Map; @@ -31,11 +34,12 @@ public class JsonFileStore implements Store> { private final Path path; - public JsonFileStore(String name, Path path) throws IOException { + public JsonFileStore(String name, Path path, long expireAfterSeconds) throws IOException { this.name = name; this.path = path; this.lock = new ReentrantReadWriteLock(); Files.createDirectories(path); + purge(expireAfterSeconds); } @Override @@ -48,7 +52,7 @@ public class JsonFileStore implements Store> { AtomicLong size = new AtomicLong(0L); try (Stream stream = Files.walk(path)) { stream.forEach(p -> { - if (Files.isRegularFile(p)) { + if (Files.isRegularFile(p) && Files.isReadable(p)) { size.incrementAndGet(); } }); @@ -57,9 +61,9 @@ public class JsonFileStore implements Store> { } @Override - public void readAll(String key, Consumer> consumer) throws IOException { + public void readAll(String prefix, Consumer> consumer) throws IOException { Objects.requireNonNull(consumer); - try (Stream stream = Files.walk(path.resolve(key))) { + try (Stream stream = Files.walk(path.resolve(prefix))) { stream.forEach(p -> { try { consumer.accept(read(p.getFileName().toString())); @@ -76,7 +80,12 @@ public class JsonFileStore implements Store> { try { readLock.lock(); PercentEncoder percentEncoder = PercentEncoders.getUnreservedEncoder(StandardCharsets.UTF_8); - return JsonUtil.toMap(Files.readString(path.resolve(percentEncoder.encode(key)))); + Path p = path.resolve(percentEncoder.encode(key)); + if (Files.isRegularFile(p) && Files.isReadable(p)) { + return JsonUtil.toMap(Files.readString(p)); + } else { + return null; + } } finally { readLock.unlock(); } @@ -92,7 +101,61 @@ public class JsonFileStore implements Store> { if (!Files.exists(p.getParent())) { Files.createDirectories(p.getParent()); } - try (Writer writer = Files.newBufferedWriter(p)) { + try (Writer writer = Files.newBufferedWriter(p, StandardOpenOption.CREATE_NEW)) { + writer.write(JsonUtil.toString(map)); + } + } finally { + writeLock.unlock(); + } + } + + @Override + public void write(String prefix, String key, Map map) throws IOException { + ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock(); + try { + writeLock.lock(); + PercentEncoder percentEncoder = PercentEncoders.getUnreservedEncoder(StandardCharsets.UTF_8); + Path p = path.resolve(prefix).resolve(percentEncoder.encode(key)); + if (!Files.exists(p.getParent())) { + Files.createDirectories(p.getParent()); + } + try (Writer writer = Files.newBufferedWriter(p, StandardOpenOption.CREATE_NEW)) { + writer.write(JsonUtil.toString(map)); + } + } finally { + writeLock.unlock(); + } + } + + @Override + public void update(String key, Map map) throws IOException { + ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock(); + try { + writeLock.lock(); + PercentEncoder percentEncoder = PercentEncoders.getUnreservedEncoder(StandardCharsets.UTF_8); + Path p = path.resolve(percentEncoder.encode(key)); + if (!Files.exists(p.getParent())) { + Files.createDirectories(p.getParent()); + } + try (Writer writer = Files.newBufferedWriter(p, StandardOpenOption.CREATE)) { + writer.write(JsonUtil.toString(map)); + } + } finally { + writeLock.unlock(); + } + } + + @Override + public void update(String prefix, String key, Map map) throws IOException { + ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock(); + try { + writeLock.lock(); + PercentEncoder percentEncoder = PercentEncoders.getUnreservedEncoder(StandardCharsets.UTF_8); + Path p = path.resolve(prefix).resolve(percentEncoder.encode(key)); + if (!Files.exists(p.getParent())) { + Files.createDirectories(p.getParent()); + } + try (Writer writer = Files.newBufferedWriter(p, StandardOpenOption.CREATE)) { writer.write(JsonUtil.toString(map)); } } finally { @@ -119,8 +182,7 @@ public class JsonFileStore implements Store> { try (Stream stream = Files.walk(path)) { stream.forEach(p -> { try { - FileTime fileTime = Files.getLastModifiedTime(p); - Duration duration = Duration.between(fileTime.toInstant(), instant); + Duration duration = Duration.between(Files.getLastModifiedTime(p).toInstant(), instant); if (duration.toSeconds() > expiredAfterSeconds) { Files.delete(p); } @@ -131,4 +193,21 @@ public class JsonFileStore implements Store> { } } } + + @Override + public void destroy() throws IOException { + Files.walkFileTree(path, new SimpleFileVisitor<>() { + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { + Files.delete(dir); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + Files.delete(file); + return FileVisitResult.CONTINUE; + } + }); + } } diff --git a/net/src/test/java/org/xbib/net/util/JsonFileStoreTest.java b/net/src/test/java/org/xbib/net/util/JsonFileStoreTest.java new file mode 100644 index 0000000..945d198 --- /dev/null +++ b/net/src/test/java/org/xbib/net/util/JsonFileStoreTest.java @@ -0,0 +1,68 @@ +package org.xbib.net.util; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class JsonFileStoreTest { + + private static final Logger logger = Logger.getLogger(JsonFileStoreTest.class.getName()); + + private static JsonFileStore jsonFileStore; + + @BeforeAll + public static void before() throws IOException { + Path path = Files.createTempDirectory("jsonstore"); + jsonFileStore = new JsonFileStore("test", path, 3600L); + } + + @AfterAll + public static void after() throws IOException { + if (jsonFileStore != null) { + jsonFileStore.destroy(); + } + } + + @Test + public void testJsonFileSTore() throws IOException { + singleReadWriteCycle(jsonFileStore, "1", Map.of("a", "b")); + } + + @Test + public void consumeJsonFileStore() throws IOException { + for (int i =0; i < 100; i++) { + String key = RandomUtil.randomString(32); + singleReadWriteCycle(jsonFileStore, key, Map.of("a", "b")); + } + jsonFileStore.readAll("", map -> logger.log(Level.INFO, "map = " + map)); + } + + @Test + public void consumeJsonFileStoreByPrefix() throws IOException { + String prefix = "DE-38"; + for (int i =0; i < 10; i++) { + String key = RandomUtil.randomString(32); + jsonFileStore.write(prefix, key, Map.of("a", "b")); + } + for (int i =0; i < 10; i++) { + String key = RandomUtil.randomString(32); + jsonFileStore.write("", key, Map.of("a", "b")); + } + jsonFileStore.readAll(prefix, map -> logger.log(Level.INFO, "map = " + map)); + } + + private void singleReadWriteCycle(JsonFileStore jsonFileStore, String key, Map map) throws IOException { + jsonFileStore.write(key, map); + Map readedMap = jsonFileStore.read(key); + assertEquals(map, readedMap); + } +}