feat(storage): Implemented StorageService
- Implemented methods for storing and loading objects and streams in StorageServiceImpl. - Added support for saving and loading streams using InputStream and OutputStream. - Enhanced ObjectSerialization to support serialization directly to OutputStream. - Updated StorageIndex to support recursive listing of items. - Added comprehensive unit tests for new functionalities in StorageServiceImpl and ObjectSerialization. - Improved error handling and logging for storage operations. - Updated AutoConfiguration to include StorageServiceAutoConfiguration.
This commit is contained in:
@@ -18,6 +18,7 @@ import java.nio.file.PathMatcher;
|
||||
public class StorageIndex {
|
||||
|
||||
private final Node root;
|
||||
|
||||
private final Map<UUID, IndexedItem> byId;
|
||||
|
||||
public StorageIndex() {
|
||||
@@ -33,6 +34,14 @@ public class StorageIndex {
|
||||
return this.byId.isEmpty();
|
||||
}
|
||||
|
||||
public List<StorageItemDescriptor> listAll() {
|
||||
List<StorageItemDescriptor> result = new ArrayList<>(this.byId.size());
|
||||
for (IndexedItem item : this.byId.values()) {
|
||||
result.add(item.descriptor());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
this.root.clear();
|
||||
this.byId.clear();
|
||||
@@ -64,17 +73,30 @@ public class StorageIndex {
|
||||
return Optional.ofNullable(node.itemsByNormalizedName.get(normalizedName)).map(IndexedItem::descriptor);
|
||||
}
|
||||
|
||||
public List<StorageItemDescriptor> list(final List<String> path) {
|
||||
public List<StorageItemDescriptor> list(final List<String> path, final boolean recursive) {
|
||||
final List<String> normalizedPath = this.normalizePath(path);
|
||||
final Node node = this.getNode(normalizedPath);
|
||||
if (node == null) {
|
||||
return List.of();
|
||||
}
|
||||
if (!recursive) {
|
||||
if (node.itemsByNormalizedName.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
return node.itemsByNormalizedName.values().stream().map(IndexedItem::descriptor).toList();
|
||||
}
|
||||
final List<StorageItemDescriptor> items = new ArrayList<>();
|
||||
final Deque<Node> work = new ArrayDeque<>();
|
||||
work.addLast(node);
|
||||
while (!work.isEmpty()) {
|
||||
final Node n = work.removeFirst();
|
||||
for (final IndexedItem item : n.itemsByNormalizedName.values()) {
|
||||
items.add(item.descriptor());
|
||||
}
|
||||
work.addAll(n.children.values());
|
||||
}
|
||||
return List.copyOf(items);
|
||||
}
|
||||
|
||||
public List<StorageItemDescriptor> findByFilenamePattern(final List<String> basePath, final String pattern, final boolean recursive) {
|
||||
Objects.requireNonNull(pattern, "pattern");
|
||||
@@ -256,7 +278,9 @@ public class StorageIndex {
|
||||
}
|
||||
|
||||
private static final class Node {
|
||||
|
||||
private final Map<String, Node> children;
|
||||
|
||||
private final Map<String, IndexedItem> itemsByNormalizedName;
|
||||
|
||||
private Node() {
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package de.extio.game_engine.storage;
|
||||
|
||||
import java.io.OutputStream;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Wrapper that provides access to an OutputStream for storing data while
|
||||
* also returning the storage UUID associated with the data.
|
||||
*/
|
||||
public record StorageOutputStream(UUID id, OutputStream outputStream) {
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package de.extio.game_engine.storage;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface StorageService {
|
||||
|
||||
boolean deleteById(UUID id);
|
||||
|
||||
boolean deleteByPath(List<String> path, String name);
|
||||
|
||||
List<StorageItemDescriptor> listAll();
|
||||
|
||||
List<StorageItemDescriptor> listPath(List<String> path, boolean recursive);
|
||||
|
||||
<T> Optional<T> loadById(Class<T> clazz, UUID id);
|
||||
|
||||
<T> Optional<T> loadByPath(Class<T> clazz, List<String> path, String name);
|
||||
|
||||
Optional<InputStream> loadStreamById(UUID id);
|
||||
|
||||
Optional<InputStream> loadStreamByPath(List<String> path, String name);
|
||||
|
||||
boolean moveById(UUID id, List<String> newPath, String newName);
|
||||
|
||||
boolean moveByPath(List<String> oldPath, String oldName, List<String> newPath, String newName);
|
||||
|
||||
Optional<StorageItemDescriptor> searchById(UUID id);
|
||||
|
||||
Optional<StorageItemDescriptor> searchByPath(List<String> path, String name);
|
||||
|
||||
List<StorageItemDescriptor> searchByPattern(List<String> basePath, String pattern, boolean recursive);
|
||||
|
||||
UUID store(String name, List<String> path, Object obj);
|
||||
|
||||
UUID storeStream(String name, List<String> path, InputStream inputStream);
|
||||
|
||||
StorageOutputStream storeStream(String name, List<String> path);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package de.extio.game_engine.storage;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
@AutoConfiguration
|
||||
@ConditionalOnProperty(name = "game-engine.storage.enabled", havingValue = "true", matchIfMissing = true)
|
||||
public class StorageServiceAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
StorageService storageService() {
|
||||
return new StorageServiceImpl();
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,25 @@
|
||||
package de.extio.game_engine.storage;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import de.extio.game_engine.util.FastRandomUUID;
|
||||
import de.extio.game_engine.util.ObjectSerialization;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class StorageServiceImpl {
|
||||
public class StorageServiceImpl implements StorageService {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(StorageServiceImpl.class);
|
||||
|
||||
@@ -83,7 +91,7 @@ public class StorageServiceImpl {
|
||||
|
||||
final StorageItemDescriptor[] descriptors;
|
||||
try (final var in = Files.newInputStream(INDEX_LOCATION)) {
|
||||
descriptors = ObjectSerialization.deserialize(StorageItemDescriptor[].class, in, false, false, null, null, null);
|
||||
descriptors = ObjectSerialization.deserialize(StorageItemDescriptor[].class, in, true, false, null, null, null);
|
||||
}
|
||||
if (descriptors == null || descriptors.length == 0) {
|
||||
return;
|
||||
@@ -103,4 +111,295 @@ public class StorageServiceImpl {
|
||||
}
|
||||
}
|
||||
|
||||
private void saveIndex() {
|
||||
try {
|
||||
final StorageItemDescriptor[] descriptors = this.index.listAll().toArray(StorageItemDescriptor[]::new);
|
||||
try (var outputStream = Files.newOutputStream(INDEX_LOCATION)) {
|
||||
ObjectSerialization.serialize(descriptors, outputStream, true, false, false, null, null);
|
||||
}
|
||||
LOGGER.debug("Saved {} items to index", descriptors.length);
|
||||
}
|
||||
catch (final Exception e) {
|
||||
LOGGER.error("Could not save index to " + INDEX_LOCATION.toString(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private Path getFilePath(final UUID id) {
|
||||
return DATA_LOCATION.resolve(id.toString());
|
||||
}
|
||||
|
||||
private void addToIndexAndSave(final UUID id, final String name, final List<String> path) {
|
||||
final StorageItemDescriptor descriptor = new StorageItemDescriptor(id, name, path == null ? List.of() : path);
|
||||
this.index.add(descriptor);
|
||||
this.saveIndex();
|
||||
}
|
||||
|
||||
@Override
|
||||
public UUID store(final String name, final List<String> path, final Object obj) {
|
||||
Objects.requireNonNull(name, "name");
|
||||
Objects.requireNonNull(obj, "obj");
|
||||
|
||||
final Optional<StorageItemDescriptor> existing = this.index.find(path, name);
|
||||
final UUID id = existing.map(StorageItemDescriptor::id).orElse(FastRandomUUID.create());
|
||||
|
||||
final Path filePath = getFilePath(id);
|
||||
try {
|
||||
try (var outputStream = Files.newOutputStream(filePath)) {
|
||||
ObjectSerialization.serialize(obj, outputStream, true, false, false, null, null);
|
||||
}
|
||||
|
||||
addToIndexAndSave(id, name, path);
|
||||
LOGGER.debug("Stored object with id {} at path {}/{}", id, path, name);
|
||||
return id;
|
||||
}
|
||||
catch (final Exception e) {
|
||||
LOGGER.error("Could not store object at " + filePath.toString(), e);
|
||||
throw new RuntimeException("Failed to store object", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public UUID storeStream(final String name, final List<String> path, final InputStream inputStream) {
|
||||
Objects.requireNonNull(name, "name");
|
||||
Objects.requireNonNull(inputStream, "inputStream");
|
||||
|
||||
final Optional<StorageItemDescriptor> existing = this.index.find(path, name);
|
||||
final UUID id = existing.map(StorageItemDescriptor::id).orElse(FastRandomUUID.create());
|
||||
|
||||
final Path filePath = getFilePath(id);
|
||||
try {
|
||||
Files.copy(inputStream, filePath, StandardCopyOption.REPLACE_EXISTING);
|
||||
|
||||
addToIndexAndSave(id, name, path);
|
||||
LOGGER.debug("Stored stream with id {} at path {}/{}", id, path, name);
|
||||
return id;
|
||||
}
|
||||
catch (final Exception e) {
|
||||
LOGGER.error("Could not store stream at " + filePath.toString(), e);
|
||||
throw new RuntimeException("Failed to store stream", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deleteById(final UUID id) {
|
||||
Objects.requireNonNull(id, "id");
|
||||
|
||||
final boolean removed = this.index.remove(id);
|
||||
if (!removed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final Path filePath = DATA_LOCATION.resolve(id.toString());
|
||||
try {
|
||||
Files.deleteIfExists(filePath);
|
||||
this.saveIndex();
|
||||
LOGGER.debug("Deleted object with id {}", id);
|
||||
return true;
|
||||
}
|
||||
catch (final IOException e) {
|
||||
LOGGER.error("Could not delete file " + filePath.toString(), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deleteByPath(final List<String> path, final String name) {
|
||||
Objects.requireNonNull(name, "name");
|
||||
|
||||
final Optional<StorageItemDescriptor> descriptor = this.index.find(path, name);
|
||||
if (descriptor.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.deleteById(descriptor.get().id());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean moveById(final UUID id, final List<String> newPath, final String newName) {
|
||||
Objects.requireNonNull(id, "id");
|
||||
Objects.requireNonNull(newName, "newName");
|
||||
|
||||
final Optional<StorageItemDescriptor> existing = this.index.getById(id);
|
||||
if (existing.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final StorageItemDescriptor newDescriptor = new StorageItemDescriptor(id, newName, newPath == null ? List.of() : newPath);
|
||||
this.index.add(newDescriptor);
|
||||
this.saveIndex();
|
||||
|
||||
LOGGER.debug("Moved object with id {} to path {}/{}", id, newPath, newName);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean moveByPath(final List<String> oldPath, final String oldName, final List<String> newPath, final String newName) {
|
||||
Objects.requireNonNull(oldName, "oldName");
|
||||
Objects.requireNonNull(newName, "newName");
|
||||
|
||||
final Optional<StorageItemDescriptor> descriptor = this.index.find(oldPath, oldName);
|
||||
if (descriptor.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.moveById(descriptor.get().id(), newPath, newName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Optional<T> loadById(final Class<T> clazz, final UUID id) {
|
||||
Objects.requireNonNull(clazz, "clazz");
|
||||
Objects.requireNonNull(id, "id");
|
||||
|
||||
if (!this.index.getById(id).isPresent()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
final Path filePath = getFilePath(id);
|
||||
if (!Files.exists(filePath)) {
|
||||
LOGGER.warn("Index references non-existent file for id {}", id);
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
try {
|
||||
try (final var in = Files.newInputStream(filePath)) {
|
||||
final T obj = ObjectSerialization.deserialize(clazz, in, true, false, null, null, null);
|
||||
return Optional.ofNullable(obj);
|
||||
}
|
||||
}
|
||||
catch (final Exception e) {
|
||||
LOGGER.error("Could not load object from " + filePath.toString(), e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Optional<T> loadByPath(final Class<T> clazz, final List<String> path, final String name) {
|
||||
Objects.requireNonNull(clazz, "clazz");
|
||||
Objects.requireNonNull(name, "name");
|
||||
|
||||
final Optional<StorageItemDescriptor> descriptor = this.index.find(path, name);
|
||||
if (descriptor.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return this.loadById(clazz, descriptor.get().id());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<InputStream> loadStreamById(final UUID id) {
|
||||
Objects.requireNonNull(id, "id");
|
||||
|
||||
if (!this.index.getById(id).isPresent()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
final Path filePath = getFilePath(id);
|
||||
if (!Files.exists(filePath)) {
|
||||
LOGGER.warn("Index references non-existent file for id {}", id);
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
try {
|
||||
return Optional.of(Files.newInputStream(filePath));
|
||||
}
|
||||
catch (final Exception e) {
|
||||
LOGGER.error("Could not load stream from " + filePath.toString(), e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<InputStream> loadStreamByPath(final List<String> path, final String name) {
|
||||
Objects.requireNonNull(name, "name");
|
||||
|
||||
final Optional<StorageItemDescriptor> descriptor = this.index.find(path, name);
|
||||
if (descriptor.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return this.loadStreamById(descriptor.get().id());
|
||||
}
|
||||
|
||||
@Override
|
||||
public StorageOutputStream storeStream(final String name, final List<String> path) {
|
||||
Objects.requireNonNull(name, "name");
|
||||
|
||||
final Optional<StorageItemDescriptor> existing = this.index.find(path, name);
|
||||
final UUID id = existing.map(StorageItemDescriptor::id).orElse(FastRandomUUID.create());
|
||||
|
||||
final Path filePath = getFilePath(id);
|
||||
try {
|
||||
final OutputStream outputStream = Files.newOutputStream(filePath);
|
||||
|
||||
return new StorageOutputStream(id, new OutputStream() {
|
||||
|
||||
private boolean closed = false;
|
||||
|
||||
@Override
|
||||
public void write(int b) throws IOException {
|
||||
outputStream.write(b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] b) throws IOException {
|
||||
outputStream.write(b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] b, int off, int len) throws IOException {
|
||||
outputStream.write(b, off, len);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() throws IOException {
|
||||
outputStream.flush();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
if (closed) {
|
||||
return;
|
||||
}
|
||||
closed = true;
|
||||
outputStream.close();
|
||||
|
||||
addToIndexAndSave(id, name, path);
|
||||
LOGGER.debug("Stored stream with id {} at path {}/{}", id, path, name);
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (final Exception e) {
|
||||
LOGGER.error("Could not create output stream at " + filePath.toString(), e);
|
||||
throw new RuntimeException("Failed to create output stream", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<StorageItemDescriptor> searchById(final UUID id) {
|
||||
Objects.requireNonNull(id, "id");
|
||||
return this.index.getById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<StorageItemDescriptor> searchByPath(final List<String> path, final String name) {
|
||||
Objects.requireNonNull(name, "name");
|
||||
return this.index.find(path, name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<StorageItemDescriptor> searchByPattern(final List<String> basePath, final String pattern, final boolean recursive) {
|
||||
Objects.requireNonNull(pattern, "pattern");
|
||||
return this.index.findByFilenamePattern(basePath, pattern, recursive);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<StorageItemDescriptor> listPath(final List<String> path, final boolean recursive) {
|
||||
return this.index.list(path, recursive);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<StorageItemDescriptor> listAll() {
|
||||
return this.index.listAll();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -31,11 +31,16 @@ public final class ObjectSerialization {
|
||||
|
||||
public static byte[] serialize(final Object o, final boolean compress, final boolean base64, final boolean digest, final byte[] digestSalt, final Consumer<byte[]> digestConsumer) {
|
||||
final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(1 << 16);
|
||||
serialize(o, byteArrayOutputStream, compress, base64, digest, digestSalt, digestConsumer);
|
||||
return byteArrayOutputStream.toByteArray();
|
||||
}
|
||||
|
||||
public static void serialize(final Object o, final OutputStream outputStream, final boolean compress, final boolean base64, final boolean digest, final byte[] digestSalt, final Consumer<byte[]> digestConsumer) {
|
||||
OutputStream base64OutputStream = null;
|
||||
ZstdOutputStream zstdOutputStream = null;
|
||||
DigestOutputStream digestOutputStream = null;
|
||||
try {
|
||||
OutputStream stream = byteArrayOutputStream;
|
||||
OutputStream stream = outputStream;
|
||||
if (base64) {
|
||||
stream = base64OutputStream = Base64.getEncoder().wrap(stream);
|
||||
}
|
||||
@@ -77,13 +82,10 @@ public final class ObjectSerialization {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return byteArrayOutputStream.toByteArray();
|
||||
}
|
||||
|
||||
public static <T extends Object> T deserialize(final Class<T> clazz, final byte[] data, final boolean decompress, final boolean base64, final byte[] checkDigest, final byte[] digestSalt, final Consumer<Boolean> digestCheckConsumer) {
|
||||
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(data);
|
||||
return deserialize(clazz, byteArrayInputStream, decompress, base64, checkDigest, digestSalt, digestCheckConsumer);
|
||||
return deserialize(clazz, new ByteArrayInputStream(data), decompress, base64, checkDigest, digestSalt, digestCheckConsumer);
|
||||
}
|
||||
|
||||
public static <T extends Object> T deserialize(final Class<T> clazz, final InputStream inputStream, final boolean decompress, final boolean base64, final byte[] checkDigest, final byte[] digestSalt, final Consumer<Boolean> digestCheckConsumer) {
|
||||
|
||||
@@ -2,3 +2,4 @@ de.extio.game_engine.event.EventsAutoConfiguration
|
||||
de.extio.game_engine.keyboard.KeycodeAutoConfiguration
|
||||
de.extio.game_engine.renderer.RendererAutoConfiguration
|
||||
de.extio.game_engine.renderer.g2d.G2DAutoConfiguration
|
||||
de.extio.game_engine.storage.StorageServiceAutoConfiguration
|
||||
|
||||
@@ -24,7 +24,7 @@ public class StorageIndexTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void listReturnsItemsOnlyAtThatDirectory() {
|
||||
void listNonRecursiveReturnsItemsOnlyAtThatDirectory() {
|
||||
final StorageIndex index = new StorageIndex();
|
||||
final StorageItemDescriptor rootItem = new StorageItemDescriptor(UUID.randomUUID(), "root", List.of());
|
||||
final StorageItemDescriptor aItem = new StorageItemDescriptor(UUID.randomUUID(), "a1", List.of("a"));
|
||||
@@ -32,10 +32,10 @@ public class StorageIndexTest {
|
||||
index.add(rootItem);
|
||||
index.add(aItem);
|
||||
index.add(abItem);
|
||||
assertThat(index.list(List.of())).containsExactlyInAnyOrder(rootItem);
|
||||
assertThat(index.list(List.of("a"))).containsExactlyInAnyOrder(aItem);
|
||||
assertThat(index.list(List.of("a", "b"))).containsExactlyInAnyOrder(abItem);
|
||||
assertThat(index.list(List.of("missing"))).isEmpty();
|
||||
assertThat(index.list(List.of(), false)).containsExactlyInAnyOrder(rootItem);
|
||||
assertThat(index.list(List.of("a"), false)).containsExactlyInAnyOrder(aItem);
|
||||
assertThat(index.list(List.of("a", "b"), false)).containsExactlyInAnyOrder(abItem);
|
||||
assertThat(index.list(List.of("missing"), false)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -45,7 +45,7 @@ public class StorageIndexTest {
|
||||
index.add(descriptor);
|
||||
assertThat(index.find(List.of("a", "b"), "x")).isPresent();
|
||||
assertThat(index.find(List.of(" ", "A", "b "), "X")).isPresent();
|
||||
assertThat(index.list(List.of("a", "b"))).hasSize(1);
|
||||
assertThat(index.list(List.of("a", "b"), false)).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -90,8 +90,8 @@ public class StorageIndexTest {
|
||||
assertThat(index.remove(leaf.id())).isTrue();
|
||||
assertThat(index.size()).isZero();
|
||||
assertThat(index.find(List.of("a", "b"), "leaf")).isEmpty();
|
||||
assertThat(index.list(List.of("a", "b"))).isEmpty();
|
||||
assertThat(index.list(List.of("a"))).isEmpty();
|
||||
assertThat(index.list(List.of("a", "b"), false)).isEmpty();
|
||||
assertThat(index.list(List.of("a"), false)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -134,4 +134,75 @@ public class StorageIndexTest {
|
||||
final var matches = index.findByFilenamePattern(List.of("assets"), "glob:*.png", true);
|
||||
assertThat(matches).containsExactlyInAnyOrder(inBase, inChild2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void listRecursiveReturnsAllItemsInDirectoryAndSubdirectories() {
|
||||
final StorageIndex index = new StorageIndex();
|
||||
final StorageItemDescriptor rootItem = new StorageItemDescriptor(UUID.randomUUID(), "root", List.of());
|
||||
final StorageItemDescriptor aItem = new StorageItemDescriptor(UUID.randomUUID(), "a1", List.of("a"));
|
||||
final StorageItemDescriptor abItem = new StorageItemDescriptor(UUID.randomUUID(), "ab1", List.of("a", "b"));
|
||||
final StorageItemDescriptor abcItem = new StorageItemDescriptor(UUID.randomUUID(), "abc1", List.of("a", "b", "c"));
|
||||
final StorageItemDescriptor otherItem = new StorageItemDescriptor(UUID.randomUUID(), "other", List.of("other"));
|
||||
index.add(rootItem);
|
||||
index.add(aItem);
|
||||
index.add(abItem);
|
||||
index.add(abcItem);
|
||||
index.add(otherItem);
|
||||
assertThat(index.list(List.of(), true)).containsExactlyInAnyOrder(rootItem, aItem, abItem, abcItem, otherItem);
|
||||
assertThat(index.list(List.of("a"), true)).containsExactlyInAnyOrder(aItem, abItem, abcItem);
|
||||
assertThat(index.list(List.of("a", "b"), true)).containsExactlyInAnyOrder(abItem, abcItem);
|
||||
assertThat(index.list(List.of("a", "b", "c"), true)).containsExactlyInAnyOrder(abcItem);
|
||||
assertThat(index.list(List.of("missing"), true)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void listRecursiveNonExistentPathReturnsEmpty() {
|
||||
final StorageIndex index = new StorageIndex();
|
||||
final StorageItemDescriptor item = new StorageItemDescriptor(UUID.randomUUID(), "file", List.of("a"));
|
||||
index.add(item);
|
||||
assertThat(index.list(List.of("nonexistent"), true)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void listRecursiveAndNonRecursiveAreConsistent() {
|
||||
final StorageIndex index = new StorageIndex();
|
||||
final StorageItemDescriptor aItem = new StorageItemDescriptor(UUID.randomUUID(), "a1", List.of("a"));
|
||||
final StorageItemDescriptor abItem = new StorageItemDescriptor(UUID.randomUUID(), "ab1", List.of("a", "b"));
|
||||
index.add(aItem);
|
||||
index.add(abItem);
|
||||
assertThat(index.list(List.of("a"), false)).containsExactly(aItem);
|
||||
assertThat(index.list(List.of("a"), true)).containsExactlyInAnyOrder(aItem, abItem);
|
||||
}
|
||||
|
||||
@Test
|
||||
void listAllReturnsAllItemsInIndex() {
|
||||
final StorageIndex index = new StorageIndex();
|
||||
final StorageItemDescriptor root = new StorageItemDescriptor(UUID.randomUUID(), "root", List.of());
|
||||
final StorageItemDescriptor a = new StorageItemDescriptor(UUID.randomUUID(), "a", List.of("a"));
|
||||
final StorageItemDescriptor ab = new StorageItemDescriptor(UUID.randomUUID(), "ab", List.of("a", "b"));
|
||||
final StorageItemDescriptor other = new StorageItemDescriptor(UUID.randomUUID(), "other", List.of("x", "y"));
|
||||
index.add(root);
|
||||
index.add(a);
|
||||
index.add(ab);
|
||||
index.add(other);
|
||||
assertThat(index.listAll()).containsExactlyInAnyOrder(root, a, ab, other);
|
||||
}
|
||||
|
||||
@Test
|
||||
void listAllOnEmptyIndexReturnsEmpty() {
|
||||
final StorageIndex index = new StorageIndex();
|
||||
assertThat(index.listAll()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void listAllIsCopyOfInternalState() {
|
||||
final StorageIndex index = new StorageIndex();
|
||||
final StorageItemDescriptor item = new StorageItemDescriptor(UUID.randomUUID(), "file", List.of("a"));
|
||||
index.add(item);
|
||||
final var list1 = index.listAll();
|
||||
index.add(new StorageItemDescriptor(UUID.randomUUID(), "file2", List.of("b")));
|
||||
final var list2 = index.listAll();
|
||||
assertThat(list1).hasSize(1);
|
||||
assertThat(list2).hasSize(2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,15 @@ package de.extio.game_engine.storage;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.lang.reflect.Field;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
@@ -33,7 +38,7 @@ class StorageServiceImplTest {
|
||||
@Test
|
||||
void loadIndexNoFileDoesNotFailAndKeepsIndexEmpty() throws Exception {
|
||||
Files.deleteIfExists(StorageServiceImpl.INDEX_LOCATION);
|
||||
final StorageServiceImpl service = new StorageServiceImpl();
|
||||
final StorageService service = new StorageServiceImpl();
|
||||
final StorageIndex index = this.getIndex(service);
|
||||
assertThat(index.isEmpty()).isTrue();
|
||||
assertThat(Files.exists(StorageServiceImpl.INDEX_LOCATION)).isFalse();
|
||||
@@ -47,20 +52,437 @@ class StorageServiceImplTest {
|
||||
final StorageItemDescriptor d1 = new StorageItemDescriptor(UUID.randomUUID(), "File1.txt", List.of("A"));
|
||||
final StorageItemDescriptor d2 = new StorageItemDescriptor(UUID.randomUUID(), "File2.txt", List.of("A", "B"));
|
||||
final StorageItemDescriptor[] descriptors = new StorageItemDescriptor[] { d1, d2 };
|
||||
final byte[] bytes = ObjectSerialization.serialize(descriptors, false, false, false, new byte[0], ignored -> {
|
||||
final byte[] bytes = ObjectSerialization.serialize(descriptors, true, false, false, new byte[0], ignored -> {
|
||||
});
|
||||
Files.write(StorageServiceImpl.INDEX_LOCATION, bytes);
|
||||
|
||||
final StorageServiceImpl service = new StorageServiceImpl();
|
||||
final StorageService service = new StorageServiceImpl();
|
||||
final StorageIndex index = this.getIndex(service);
|
||||
assertThat(index.size()).isEqualTo(2);
|
||||
assertThat(index.find(List.of("a"), "file1.txt")).contains(d1);
|
||||
assertThat(index.find(List.of("a", "b"), "file2.txt")).contains(d2);
|
||||
}
|
||||
|
||||
private StorageIndex getIndex(final StorageServiceImpl service) throws Exception {
|
||||
@Test
|
||||
void storeCreatesNewObjectAndReturnsUUID() throws Exception {
|
||||
Files.deleteIfExists(StorageServiceImpl.INDEX_LOCATION);
|
||||
final StorageService service = new StorageServiceImpl();
|
||||
|
||||
final TestData data = new TestData("test", 123);
|
||||
final UUID id = service.store("myfile.dat", List.of("folder", "subfolder"), data);
|
||||
|
||||
assertThat(id).isNotNull();
|
||||
assertThat(Files.exists(StorageServiceImpl.DATA_LOCATION.resolve(id.toString()))).isTrue();
|
||||
|
||||
final StorageIndex index = this.getIndex(service);
|
||||
final Optional<StorageItemDescriptor> descriptor = index.find(List.of("folder", "subfolder"), "myfile.dat");
|
||||
assertThat(descriptor).isPresent();
|
||||
assertThat(descriptor.get().id()).isEqualTo(id);
|
||||
assertThat(descriptor.get().name()).isEqualTo("myfile.dat");
|
||||
assertThat(descriptor.get().path()).containsExactly("folder", "subfolder");
|
||||
}
|
||||
|
||||
@Test
|
||||
void storeStreamCreatesNewObjectAndReturnsUUID() throws Exception {
|
||||
Files.deleteIfExists(StorageServiceImpl.INDEX_LOCATION);
|
||||
final StorageService service = new StorageServiceImpl();
|
||||
|
||||
final String content = "Hello World";
|
||||
final InputStream inputStream = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8));
|
||||
final UUID id = service.storeStream("mystream.txt", List.of("streams"), inputStream);
|
||||
|
||||
assertThat(id).isNotNull();
|
||||
assertThat(Files.exists(StorageServiceImpl.DATA_LOCATION.resolve(id.toString()))).isTrue();
|
||||
|
||||
final StorageIndex index = this.getIndex(service);
|
||||
final Optional<StorageItemDescriptor> descriptor = index.find(List.of("streams"), "mystream.txt");
|
||||
assertThat(descriptor).isPresent();
|
||||
assertThat(descriptor.get().id()).isEqualTo(id);
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadStreamByIdReturnsInputStream() throws Exception {
|
||||
Files.deleteIfExists(StorageServiceImpl.INDEX_LOCATION);
|
||||
final StorageService service = new StorageServiceImpl();
|
||||
|
||||
final String content = "Stream Content";
|
||||
final InputStream inputStream = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8));
|
||||
final UUID id = service.storeStream("mystream.txt", List.of("streams"), inputStream);
|
||||
|
||||
final Optional<InputStream> loaded = service.loadStreamById(id);
|
||||
assertThat(loaded).isPresent();
|
||||
try (InputStream in = loaded.get()) {
|
||||
final String loadedContent = new String(in.readAllBytes(), StandardCharsets.UTF_8);
|
||||
assertThat(loadedContent).isEqualTo(content);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadStreamByPathReturnsInputStream() throws Exception {
|
||||
Files.deleteIfExists(StorageServiceImpl.INDEX_LOCATION);
|
||||
final StorageService service = new StorageServiceImpl();
|
||||
|
||||
final String content = "Path Stream Content";
|
||||
final InputStream inputStream = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8));
|
||||
service.storeStream("pathstream.txt", List.of("path", "streams"), inputStream);
|
||||
|
||||
final Optional<InputStream> loaded = service.loadStreamByPath(List.of("path", "streams"), "pathstream.txt");
|
||||
assertThat(loaded).isPresent();
|
||||
try (InputStream in = loaded.get()) {
|
||||
final String loadedContent = new String(in.readAllBytes(), StandardCharsets.UTF_8);
|
||||
assertThat(loadedContent).isEqualTo(content);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void storeStreamWithOutputStreamCreatesNewObjectAndReturnsUUIDAndOutputStream() throws Exception {
|
||||
Files.deleteIfExists(StorageServiceImpl.INDEX_LOCATION);
|
||||
final StorageService service = new StorageServiceImpl();
|
||||
|
||||
final StorageOutputStream storageOutput = service.storeStream("outstream.txt", List.of("output", "streams"));
|
||||
assertThat(storageOutput.id()).isNotNull();
|
||||
assertThat(storageOutput.outputStream()).isNotNull();
|
||||
|
||||
final String content = "Output Stream Content";
|
||||
try (OutputStream out = storageOutput.outputStream()) {
|
||||
out.write(content.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
assertThat(Files.exists(StorageServiceImpl.DATA_LOCATION.resolve(storageOutput.id().toString()))).isTrue();
|
||||
|
||||
final StorageIndex index = this.getIndex(service);
|
||||
final Optional<StorageItemDescriptor> descriptor = index.find(List.of("output", "streams"), "outstream.txt");
|
||||
assertThat(descriptor).isPresent();
|
||||
assertThat(descriptor.get().id()).isEqualTo(storageOutput.id());
|
||||
}
|
||||
|
||||
@Test
|
||||
void storeStreamWithOutputStreamCanBeReadBack() throws Exception {
|
||||
Files.deleteIfExists(StorageServiceImpl.INDEX_LOCATION);
|
||||
final StorageService service = new StorageServiceImpl();
|
||||
|
||||
final String content = "Output Stream Readback Content";
|
||||
final StorageOutputStream storageOutput = service.storeStream("readback.txt", List.of("readback"));
|
||||
try (OutputStream out = storageOutput.outputStream()) {
|
||||
out.write(content.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
final Optional<InputStream> loaded = service.loadStreamById(storageOutput.id());
|
||||
assertThat(loaded).isPresent();
|
||||
try (InputStream in = loaded.get()) {
|
||||
final String loadedContent = new String(in.readAllBytes(), StandardCharsets.UTF_8);
|
||||
assertThat(loadedContent).isEqualTo(content);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void storeReplacesExistingObjectWithSamePathAndName() throws Exception {
|
||||
Files.deleteIfExists(StorageServiceImpl.INDEX_LOCATION);
|
||||
final StorageServiceImpl service = new StorageServiceImpl();
|
||||
|
||||
final TestData data1 = new TestData("first", 100);
|
||||
final UUID id1 = service.store("myfile.dat", List.of("folder"), data1);
|
||||
|
||||
final TestData data2 = new TestData("second", 200);
|
||||
final UUID id2 = service.store("myfile.dat", List.of("folder"), data2);
|
||||
|
||||
assertThat(id1).isEqualTo(id2);
|
||||
|
||||
final Optional<TestData> loaded = service.loadById(TestData.class, id2);
|
||||
assertThat(loaded).isPresent();
|
||||
assertThat(loaded.get().value()).isEqualTo("second");
|
||||
assertThat(loaded.get().number()).isEqualTo(200);
|
||||
|
||||
final StorageIndex index = this.getIndex(service);
|
||||
assertThat(index.size()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteByIdRemovesObjectAndFile() throws Exception {
|
||||
Files.deleteIfExists(StorageServiceImpl.INDEX_LOCATION);
|
||||
final StorageService service = new StorageServiceImpl();
|
||||
|
||||
final TestData data = new TestData("test", 123);
|
||||
final UUID id = service.store("myfile.dat", List.of("folder"), data);
|
||||
|
||||
final Path filePath = StorageServiceImpl.DATA_LOCATION.resolve(id.toString());
|
||||
assertThat(Files.exists(filePath)).isTrue();
|
||||
|
||||
final boolean deleted = service.deleteById(id);
|
||||
assertThat(deleted).isTrue();
|
||||
assertThat(Files.exists(filePath)).isFalse();
|
||||
|
||||
final StorageIndex index = this.getIndex(service);
|
||||
assertThat(index.getById(id)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteByIdReturnsFalseForNonExistentId() throws Exception {
|
||||
Files.deleteIfExists(StorageServiceImpl.INDEX_LOCATION);
|
||||
final StorageService service = new StorageServiceImpl();
|
||||
|
||||
final boolean deleted = service.deleteById(UUID.randomUUID());
|
||||
assertThat(deleted).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteByPathRemovesObject() throws Exception {
|
||||
Files.deleteIfExists(StorageServiceImpl.INDEX_LOCATION);
|
||||
final StorageService service = new StorageServiceImpl();
|
||||
|
||||
final TestData data = new TestData("test", 123);
|
||||
final UUID id = service.store("myfile.dat", List.of("folder"), data);
|
||||
|
||||
final boolean deleted = service.deleteByPath(List.of("folder"), "myfile.dat");
|
||||
assertThat(deleted).isTrue();
|
||||
|
||||
final StorageIndex index = this.getIndex(service);
|
||||
assertThat(index.getById(id)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteByPathReturnsFalseForNonExistentPath() throws Exception {
|
||||
Files.deleteIfExists(StorageServiceImpl.INDEX_LOCATION);
|
||||
final StorageService service = new StorageServiceImpl();
|
||||
|
||||
final boolean deleted = service.deleteByPath(List.of("nonexistent"), "file.dat");
|
||||
assertThat(deleted).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void moveByIdUpdatesPathAndName() throws Exception {
|
||||
Files.deleteIfExists(StorageServiceImpl.INDEX_LOCATION);
|
||||
final StorageService service = new StorageServiceImpl();
|
||||
|
||||
final TestData data = new TestData("test", 123);
|
||||
final UUID id = service.store("oldfile.dat", List.of("old", "path"), data);
|
||||
|
||||
final boolean moved = service.moveById(id, List.of("new", "path"), "newfile.dat");
|
||||
assertThat(moved).isTrue();
|
||||
|
||||
final StorageIndex index = this.getIndex(service);
|
||||
final Optional<StorageItemDescriptor> oldLocation = index.find(List.of("old", "path"), "oldfile.dat");
|
||||
assertThat(oldLocation).isEmpty();
|
||||
|
||||
final Optional<StorageItemDescriptor> newLocation = index.find(List.of("new", "path"), "newfile.dat");
|
||||
assertThat(newLocation).isPresent();
|
||||
assertThat(newLocation.get().id()).isEqualTo(id);
|
||||
}
|
||||
|
||||
@Test
|
||||
void moveByIdReturnsFalseForNonExistentId() throws Exception {
|
||||
Files.deleteIfExists(StorageServiceImpl.INDEX_LOCATION);
|
||||
final StorageService service = new StorageServiceImpl();
|
||||
|
||||
final boolean moved = service.moveById(UUID.randomUUID(), List.of("new"), "file.dat");
|
||||
assertThat(moved).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void moveByPathUpdatesLocation() throws Exception {
|
||||
Files.deleteIfExists(StorageServiceImpl.INDEX_LOCATION);
|
||||
final StorageService service = new StorageServiceImpl();
|
||||
|
||||
final TestData data = new TestData("test", 123);
|
||||
service.store("oldfile.dat", List.of("old"), data);
|
||||
|
||||
final boolean moved = service.moveByPath(List.of("old"), "oldfile.dat", List.of("new"), "newfile.dat");
|
||||
assertThat(moved).isTrue();
|
||||
|
||||
final StorageIndex index = this.getIndex(service);
|
||||
final Optional<StorageItemDescriptor> newLocation = index.find(List.of("new"), "newfile.dat");
|
||||
assertThat(newLocation).isPresent();
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadByIdDeserializesObject() throws Exception {
|
||||
Files.deleteIfExists(StorageServiceImpl.INDEX_LOCATION);
|
||||
final StorageServiceImpl service = new StorageServiceImpl();
|
||||
|
||||
final TestData data = new TestData("test", 456);
|
||||
final UUID id = service.store("myfile.dat", List.of("folder"), data);
|
||||
|
||||
final Optional<TestData> loaded = service.loadById(TestData.class, id);
|
||||
assertThat(loaded).isPresent();
|
||||
assertThat(loaded.get().value()).isEqualTo("test");
|
||||
assertThat(loaded.get().number()).isEqualTo(456);
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadByIdReturnsEmptyForNonExistentId() throws Exception {
|
||||
Files.deleteIfExists(StorageServiceImpl.INDEX_LOCATION);
|
||||
final StorageServiceImpl service = new StorageServiceImpl();
|
||||
|
||||
final Optional<TestData> loaded = service.loadById(TestData.class, UUID.randomUUID());
|
||||
assertThat(loaded).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadByPathDeserializesObject() throws Exception {
|
||||
Files.deleteIfExists(StorageServiceImpl.INDEX_LOCATION);
|
||||
final StorageServiceImpl service = new StorageServiceImpl();
|
||||
|
||||
final TestData data = new TestData("test", 789);
|
||||
service.store("myfile.dat", List.of("folder", "sub"), data);
|
||||
|
||||
final Optional<TestData> loaded = service.loadByPath(TestData.class, List.of("folder", "sub"), "myfile.dat");
|
||||
assertThat(loaded).isPresent();
|
||||
assertThat(loaded.get().value()).isEqualTo("test");
|
||||
assertThat(loaded.get().number()).isEqualTo(789);
|
||||
}
|
||||
|
||||
@Test
|
||||
void searchByIdFindsDescriptor() throws Exception {
|
||||
Files.deleteIfExists(StorageServiceImpl.INDEX_LOCATION);
|
||||
final StorageService service = new StorageServiceImpl();
|
||||
|
||||
final TestData data = new TestData("test", 123);
|
||||
final UUID id = service.store("myfile.dat", List.of("folder"), data);
|
||||
|
||||
final Optional<StorageItemDescriptor> descriptor = service.searchById(id);
|
||||
assertThat(descriptor).isPresent();
|
||||
assertThat(descriptor.get().id()).isEqualTo(id);
|
||||
assertThat(descriptor.get().name()).isEqualTo("myfile.dat");
|
||||
}
|
||||
|
||||
@Test
|
||||
void searchByPathFindsDescriptor() throws Exception {
|
||||
Files.deleteIfExists(StorageServiceImpl.INDEX_LOCATION);
|
||||
final StorageService service = new StorageServiceImpl();
|
||||
|
||||
final TestData data = new TestData("test", 123);
|
||||
final UUID id = service.store("myfile.dat", List.of("folder"), data);
|
||||
|
||||
final Optional<StorageItemDescriptor> descriptor = service.searchByPath(List.of("folder"), "myfile.dat");
|
||||
assertThat(descriptor).isPresent();
|
||||
assertThat(descriptor.get().id()).isEqualTo(id);
|
||||
}
|
||||
|
||||
@Test
|
||||
void searchByPatternFindsMatchingFiles() throws Exception {
|
||||
Files.deleteIfExists(StorageServiceImpl.INDEX_LOCATION);
|
||||
final StorageService service = new StorageServiceImpl();
|
||||
|
||||
service.store("file1.txt", List.of("docs"), new TestData("a", 1));
|
||||
service.store("file2.txt", List.of("docs"), new TestData("b", 2));
|
||||
service.store("data.dat", List.of("docs"), new TestData("c", 3));
|
||||
|
||||
final List<StorageItemDescriptor> matches = service.searchByPattern(List.of("docs"), "*.txt", false);
|
||||
assertThat(matches).hasSize(2);
|
||||
assertThat(matches.stream().map(StorageItemDescriptor::name)).containsExactlyInAnyOrder("file1.txt", "file2.txt");
|
||||
}
|
||||
|
||||
@Test
|
||||
void searchByPatternRecursiveSearchesSubdirectories() throws Exception {
|
||||
Files.deleteIfExists(StorageServiceImpl.INDEX_LOCATION);
|
||||
final StorageService service = new StorageServiceImpl();
|
||||
|
||||
service.store("root.txt", List.of(), new TestData("a", 1));
|
||||
service.store("sub1.txt", List.of("level1"), new TestData("b", 2));
|
||||
service.store("sub2.txt", List.of("level1", "level2"), new TestData("c", 3));
|
||||
|
||||
final List<StorageItemDescriptor> matches = service.searchByPattern(List.of(), "*.txt", true);
|
||||
assertThat(matches).hasSize(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void listPathReturnsAllItemsInPath() throws Exception {
|
||||
Files.deleteIfExists(StorageServiceImpl.INDEX_LOCATION);
|
||||
final StorageService service = new StorageServiceImpl();
|
||||
|
||||
service.store("file1.txt", List.of("docs"), new TestData("a", 1));
|
||||
service.store("file2.txt", List.of("docs"), new TestData("b", 2));
|
||||
service.store("other.txt", List.of("other"), new TestData("c", 3));
|
||||
|
||||
final List<StorageItemDescriptor> items = service.listPath(List.of("docs"), false);
|
||||
assertThat(items).hasSize(2);
|
||||
assertThat(items.stream().map(StorageItemDescriptor::name)).containsExactlyInAnyOrder("file1.txt", "file2.txt");
|
||||
}
|
||||
|
||||
@Test
|
||||
void listPathNonRecursiveReturnsOnlyDirectItems() throws Exception {
|
||||
Files.deleteIfExists(StorageServiceImpl.INDEX_LOCATION);
|
||||
final StorageService service = new StorageServiceImpl();
|
||||
|
||||
service.store("root.txt", List.of("docs"), new TestData("a", 1));
|
||||
service.store("sub1.txt", List.of("docs", "subdir"), new TestData("b", 2));
|
||||
service.store("sub2.txt", List.of("docs", "subdir", "deeper"), new TestData("c", 3));
|
||||
|
||||
final List<StorageItemDescriptor> items = service.listPath(List.of("docs"), false);
|
||||
assertThat(items).hasSize(1);
|
||||
assertThat(items.get(0).name()).isEqualTo("root.txt");
|
||||
}
|
||||
|
||||
@Test
|
||||
void listPathRecursiveReturnsAllItemsInSubdirectories() throws Exception {
|
||||
Files.deleteIfExists(StorageServiceImpl.INDEX_LOCATION);
|
||||
final StorageService service = new StorageServiceImpl();
|
||||
|
||||
service.store("root.txt", List.of("docs"), new TestData("a", 1));
|
||||
service.store("sub1.txt", List.of("docs", "subdir"), new TestData("b", 2));
|
||||
service.store("sub2.txt", List.of("docs", "subdir", "deeper"), new TestData("c", 3));
|
||||
|
||||
final List<StorageItemDescriptor> items = service.listPath(List.of("docs"), true);
|
||||
assertThat(items).hasSize(3);
|
||||
assertThat(items.stream().map(StorageItemDescriptor::name)).containsExactlyInAnyOrder("root.txt", "sub1.txt", "sub2.txt");
|
||||
}
|
||||
|
||||
@Test
|
||||
void listPathRecursiveFromRootReturnsAllItems() throws Exception {
|
||||
Files.deleteIfExists(StorageServiceImpl.INDEX_LOCATION);
|
||||
final StorageService service = new StorageServiceImpl();
|
||||
|
||||
service.store("file1.txt", List.of(), new TestData("a", 1));
|
||||
service.store("file2.txt", List.of("level1"), new TestData("b", 2));
|
||||
service.store("file3.txt", List.of("level1", "level2"), new TestData("c", 3));
|
||||
service.store("file4.txt", List.of("other"), new TestData("d", 4));
|
||||
|
||||
final List<StorageItemDescriptor> items = service.listPath(List.of(), true);
|
||||
assertThat(items).hasSize(4);
|
||||
assertThat(items.stream().map(StorageItemDescriptor::name)).containsExactlyInAnyOrder("file1.txt", "file2.txt", "file3.txt", "file4.txt");
|
||||
}
|
||||
|
||||
@Test
|
||||
void listAllReturnsAllStoredItems() throws Exception {
|
||||
Files.deleteIfExists(StorageServiceImpl.INDEX_LOCATION);
|
||||
final StorageService service = new StorageServiceImpl();
|
||||
|
||||
service.store("file1.txt", List.of("docs"), new TestData("a", 1));
|
||||
service.store("file2.txt", List.of("data"), new TestData("b", 2));
|
||||
service.store("file3.txt", List.of("docs", "sub"), new TestData("c", 3));
|
||||
|
||||
final List<StorageItemDescriptor> items = service.listAll();
|
||||
assertThat(items).hasSize(3);
|
||||
assertThat(items.stream().map(StorageItemDescriptor::name)).containsExactlyInAnyOrder("file1.txt", "file2.txt", "file3.txt");
|
||||
}
|
||||
|
||||
@Test
|
||||
void listAllOnEmptyServiceReturnsEmpty() throws Exception {
|
||||
Files.deleteIfExists(StorageServiceImpl.INDEX_LOCATION);
|
||||
final StorageService service = new StorageServiceImpl();
|
||||
|
||||
final List<StorageItemDescriptor> items = service.listAll();
|
||||
assertThat(items).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void listAllAfterDeletionReflectsRemovedItems() throws Exception {
|
||||
Files.deleteIfExists(StorageServiceImpl.INDEX_LOCATION);
|
||||
final StorageService service = new StorageServiceImpl();
|
||||
|
||||
final UUID id = service.store("file.txt", List.of("docs"), new TestData("test", 1));
|
||||
assertThat(service.listAll()).hasSize(1);
|
||||
|
||||
service.deleteById(id);
|
||||
assertThat(service.listAll()).isEmpty();
|
||||
}
|
||||
|
||||
private StorageIndex getIndex(final StorageService service) throws Exception {
|
||||
final Field field = StorageServiceImpl.class.getDeclaredField("index");
|
||||
field.setAccessible(true);
|
||||
return (StorageIndex) field.get(service);
|
||||
}
|
||||
|
||||
public static record TestData(String value, int number) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -133,6 +134,112 @@ public class ObjectSerializationTest {
|
||||
assertEquals(original.value, deserialized.value);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSerializeOutputStreamBasic() {
|
||||
var original = new TestObject("OutputStreamTest", 42, 3.14, List.of("tag1", "tag2"), List.of(new TestObject2("key", true)), Map.of("attr", 1));
|
||||
var outputStream = new ByteArrayOutputStream();
|
||||
|
||||
ObjectSerialization.serialize(original, outputStream, false, false, false, null, digest -> {});
|
||||
|
||||
var serialized = outputStream.toByteArray();
|
||||
assertNotNull(serialized);
|
||||
assertTrue(serialized.length > 0);
|
||||
|
||||
var deserialized = ObjectSerialization.deserialize(TestObject.class, serialized, false, false, null, null, result -> {});
|
||||
|
||||
assertNotNull(deserialized);
|
||||
assertEquals(original.name, deserialized.name);
|
||||
assertEquals(original.id, deserialized.id);
|
||||
assertEquals(original.value, deserialized.value);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSerializeOutputStreamWithCompression() {
|
||||
var original = new TestObject("CompressedStream", 100, 2.71, List.of("a", "b", "c"), List.of(), Map.of());
|
||||
var outputStream = new ByteArrayOutputStream();
|
||||
|
||||
ObjectSerialization.serialize(original, outputStream, true, false, false, null, digest -> {});
|
||||
|
||||
var serialized = outputStream.toByteArray();
|
||||
assertNotNull(serialized);
|
||||
assertTrue(serialized.length > 0);
|
||||
|
||||
var deserialized = ObjectSerialization.deserialize(TestObject.class, serialized, true, false, null, null, result -> {});
|
||||
|
||||
assertNotNull(deserialized);
|
||||
assertEquals(original.name, deserialized.name);
|
||||
assertEquals(original.id, deserialized.id);
|
||||
assertEquals(original.value, deserialized.value);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSerializeOutputStreamWithBase64() {
|
||||
var original = new TestObject("Base64Stream", 200, 1.41, List.of(), List.of(), Map.of());
|
||||
var outputStream = new ByteArrayOutputStream();
|
||||
|
||||
ObjectSerialization.serialize(original, outputStream, false, true, false, null, digest -> {});
|
||||
|
||||
var serialized = outputStream.toByteArray();
|
||||
assertNotNull(serialized);
|
||||
assertTrue(serialized.length > 0);
|
||||
assertTrue(new String(serialized).matches("[A-Za-z0-9+/=]+"));
|
||||
|
||||
var deserialized = ObjectSerialization.deserialize(TestObject.class, serialized, false, true, null, null, result -> {});
|
||||
|
||||
assertNotNull(deserialized);
|
||||
assertEquals(original.name, deserialized.name);
|
||||
assertEquals(original.id, deserialized.id);
|
||||
assertEquals(original.value, deserialized.value);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSerializeOutputStreamWithDigest() {
|
||||
var original = new TestObject("DigestedStream", 300, 0.5, List.of("x"), List.of(), Map.of());
|
||||
var digestSalt = "salt".getBytes();
|
||||
var capturedDigest = new byte[32][];
|
||||
var outputStream = new ByteArrayOutputStream();
|
||||
|
||||
ObjectSerialization.serialize(original, outputStream, false, false, true, digestSalt, digest -> capturedDigest[0] = digest);
|
||||
|
||||
var serialized = outputStream.toByteArray();
|
||||
assertNotNull(serialized);
|
||||
assertNotNull(capturedDigest[0]);
|
||||
assertTrue(capturedDigest[0].length == 32);
|
||||
|
||||
var digestCheckPassed = new boolean[1];
|
||||
var deserialized = ObjectSerialization.deserialize(TestObject.class, serialized, false, false, capturedDigest[0], digestSalt, result -> digestCheckPassed[0] = result);
|
||||
|
||||
assertNotNull(deserialized);
|
||||
assertTrue(digestCheckPassed[0]);
|
||||
assertEquals(original.name, deserialized.name);
|
||||
assertEquals(original.id, deserialized.id);
|
||||
assertEquals(original.value, deserialized.value);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSerializeOutputStreamWithAllOptions() {
|
||||
var original = new TestObject("AllOptionsStream", 500, 9.99, List.of("all"), List.of(new TestObject2("a", false), new TestObject2("b", true)), Map.of("x", 1, "y", 2));
|
||||
var digestSalt = "salt123".getBytes();
|
||||
var capturedDigest = new byte[32][];
|
||||
var outputStream = new ByteArrayOutputStream();
|
||||
|
||||
ObjectSerialization.serialize(original, outputStream, true, true, true, digestSalt, digest -> capturedDigest[0] = digest);
|
||||
|
||||
var serialized = outputStream.toByteArray();
|
||||
assertNotNull(serialized);
|
||||
assertNotNull(capturedDigest[0]);
|
||||
assertTrue(new String(serialized).matches("[A-Za-z0-9+/=]+"));
|
||||
|
||||
var digestCheckPassed = new boolean[1];
|
||||
var deserialized = ObjectSerialization.deserialize(TestObject.class, serialized, true, true, capturedDigest[0], digestSalt, result -> digestCheckPassed[0] = result);
|
||||
|
||||
assertNotNull(deserialized);
|
||||
assertTrue(digestCheckPassed[0]);
|
||||
assertEquals(original.name, deserialized.name);
|
||||
assertEquals(original.id, deserialized.id);
|
||||
assertEquals(original.value, deserialized.value);
|
||||
}
|
||||
|
||||
public record TestObject(String name, int id, double value, List<String> tags, List<TestObject2> children, Map<String, Integer> attributes) {
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user