Add StorageIndex and StorageItemDescriptor classes with basic functionality; implement StorageServiceImpl for managing storage and loading index; include unit tests for StorageIndex and StorageServiceImpl
This commit is contained in:
276
src/main/java/de/extio/game_engine/storage/StorageIndex.java
Normal file
276
src/main/java/de/extio/game_engine/storage/StorageIndex.java
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
package de.extio.game_engine.storage;
|
||||||
|
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Deque;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.nio.file.FileSystems;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.PathMatcher;
|
||||||
|
|
||||||
|
public class StorageIndex {
|
||||||
|
|
||||||
|
private final Node root;
|
||||||
|
private final Map<UUID, IndexedItem> byId;
|
||||||
|
|
||||||
|
public StorageIndex() {
|
||||||
|
this.root = new Node();
|
||||||
|
this.byId = new HashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int size() {
|
||||||
|
return this.byId.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isEmpty() {
|
||||||
|
return this.byId.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clear() {
|
||||||
|
this.root.clear();
|
||||||
|
this.byId.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void add(final StorageItemDescriptor descriptor) {
|
||||||
|
Objects.requireNonNull(descriptor, "descriptor");
|
||||||
|
Objects.requireNonNull(descriptor.id(), "descriptor.id");
|
||||||
|
Objects.requireNonNull(descriptor.name(), "descriptor.name");
|
||||||
|
final String normalizedName = this.normalizeName(descriptor.name());
|
||||||
|
final List<String> normalizedPath = this.normalizePath(descriptor.path());
|
||||||
|
final IndexedItem indexedItem = new IndexedItem(descriptor, normalizedPath, normalizedName);
|
||||||
|
this.insert(indexedItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<StorageItemDescriptor> getById(final UUID id) {
|
||||||
|
Objects.requireNonNull(id, "id");
|
||||||
|
return Optional.ofNullable(this.byId.get(id)).map(IndexedItem::descriptor);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<StorageItemDescriptor> find(final List<String> path, final String name) {
|
||||||
|
Objects.requireNonNull(name, "name");
|
||||||
|
final List<String> normalizedPath = this.normalizePath(path);
|
||||||
|
final String normalizedName = this.normalizeName(name);
|
||||||
|
final Node node = this.getNode(normalizedPath);
|
||||||
|
if (node == null) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
return Optional.ofNullable(node.itemsByNormalizedName.get(normalizedName)).map(IndexedItem::descriptor);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<StorageItemDescriptor> list(final List<String> path) {
|
||||||
|
final List<String> normalizedPath = this.normalizePath(path);
|
||||||
|
final Node node = this.getNode(normalizedPath);
|
||||||
|
if (node == null) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
if (node.itemsByNormalizedName.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
return node.itemsByNormalizedName.values().stream().map(IndexedItem::descriptor).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<StorageItemDescriptor> findByFilenamePattern(final List<String> basePath, final String pattern, final boolean recursive) {
|
||||||
|
Objects.requireNonNull(pattern, "pattern");
|
||||||
|
final List<String> normalizedBasePath = this.normalizePath(basePath);
|
||||||
|
final Node baseNode = this.getNode(normalizedBasePath);
|
||||||
|
if (baseNode == null) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
final PathMatcher matcher = this.createCaseInsensitiveFilenameMatcher(pattern);
|
||||||
|
final List<StorageItemDescriptor> matches = new ArrayList<>();
|
||||||
|
if (!recursive) {
|
||||||
|
this.collectMatches(baseNode, matcher, matches);
|
||||||
|
return List.copyOf(matches);
|
||||||
|
}
|
||||||
|
final Deque<Node> work = new ArrayDeque<>();
|
||||||
|
work.addLast(baseNode);
|
||||||
|
while (!work.isEmpty()) {
|
||||||
|
final Node n = work.removeFirst();
|
||||||
|
this.collectMatches(n, matcher, matches);
|
||||||
|
work.addAll(n.children.values());
|
||||||
|
}
|
||||||
|
return List.copyOf(matches);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean remove(final UUID id) {
|
||||||
|
Objects.requireNonNull(id, "id");
|
||||||
|
final IndexedItem indexedItem = this.byId.remove(id);
|
||||||
|
if (indexedItem == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final Node node = this.getNode(indexedItem.normalizedPath());
|
||||||
|
if (node != null) {
|
||||||
|
final IndexedItem existing = node.itemsByNormalizedName.get(indexedItem.normalizedName());
|
||||||
|
if (existing != null && existing.descriptor().id().equals(id)) {
|
||||||
|
node.itemsByNormalizedName.remove(indexedItem.normalizedName());
|
||||||
|
this.pruneEmptyParents(indexedItem.normalizedPath());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean remove(final List<String> path, final String name) {
|
||||||
|
Objects.requireNonNull(name, "name");
|
||||||
|
final List<String> normalizedPath = this.normalizePath(path);
|
||||||
|
final String normalizedName = this.normalizeName(name);
|
||||||
|
final Node node = this.getNode(normalizedPath);
|
||||||
|
if (node == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final IndexedItem removed = node.itemsByNormalizedName.remove(normalizedName);
|
||||||
|
if (removed == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this.byId.remove(removed.descriptor().id());
|
||||||
|
this.pruneEmptyParents(normalizedPath);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void insert(final IndexedItem indexedItem) {
|
||||||
|
final IndexedItem existingById = this.byId.get(indexedItem.descriptor().id());
|
||||||
|
if (existingById != null) {
|
||||||
|
this.remove(existingById.descriptor().id());
|
||||||
|
}
|
||||||
|
final Node node = this.getOrCreateNode(indexedItem.normalizedPath());
|
||||||
|
final IndexedItem existingByName = node.itemsByNormalizedName.get(indexedItem.normalizedName());
|
||||||
|
if (existingByName != null) {
|
||||||
|
this.byId.remove(existingByName.descriptor().id());
|
||||||
|
}
|
||||||
|
node.itemsByNormalizedName.put(indexedItem.normalizedName(), indexedItem);
|
||||||
|
this.byId.put(indexedItem.descriptor().id(), indexedItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> normalizePath(final List<String> path) {
|
||||||
|
if (path == null || path.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
final List<String> normalized = new ArrayList<>(path.size());
|
||||||
|
for (final String segment : path) {
|
||||||
|
if (segment == null) {
|
||||||
|
throw new IllegalArgumentException("path segment must not be null");
|
||||||
|
}
|
||||||
|
final String trimmed = segment.trim();
|
||||||
|
if (!trimmed.isEmpty()) {
|
||||||
|
normalized.add(trimmed.toLowerCase(Locale.ROOT));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Collections.unmodifiableList(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeName(final String name) {
|
||||||
|
final String trimmed = Objects.requireNonNull(name, "name").trim();
|
||||||
|
if (trimmed.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("name must not be empty");
|
||||||
|
}
|
||||||
|
return trimmed.toLowerCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PathMatcher createCaseInsensitiveFilenameMatcher(final String pattern) {
|
||||||
|
final String trimmed = pattern.trim();
|
||||||
|
if (trimmed.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("pattern must not be empty");
|
||||||
|
}
|
||||||
|
final String syntax;
|
||||||
|
final String body;
|
||||||
|
if (trimmed.startsWith("glob:") || trimmed.startsWith("regex:")) {
|
||||||
|
final int sep = trimmed.indexOf(':');
|
||||||
|
syntax = trimmed.substring(0, sep);
|
||||||
|
body = trimmed.substring(sep + 1);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
syntax = "glob";
|
||||||
|
body = trimmed;
|
||||||
|
}
|
||||||
|
final String normalized;
|
||||||
|
if ("glob".equals(syntax)) {
|
||||||
|
normalized = "glob:" + body.toLowerCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
final String regexBody = body.startsWith("(?") ? body : "(?iu)" + body;
|
||||||
|
normalized = "regex:" + regexBody;
|
||||||
|
}
|
||||||
|
return FileSystems.getDefault().getPathMatcher(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void collectMatches(final Node node, final PathMatcher matcher, final List<StorageItemDescriptor> matches) {
|
||||||
|
for (final IndexedItem item : node.itemsByNormalizedName.values()) {
|
||||||
|
if (matcher.matches(Path.of(item.normalizedName()))) {
|
||||||
|
matches.add(item.descriptor());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Node getNode(final List<String> path) {
|
||||||
|
Node current = this.root;
|
||||||
|
for (final String segment : path) {
|
||||||
|
current = current.children.get(segment);
|
||||||
|
if (current == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Node getOrCreateNode(final List<String> path) {
|
||||||
|
Node current = this.root;
|
||||||
|
for (final String segment : path) {
|
||||||
|
current = current.children.computeIfAbsent(segment, ignored -> new Node());
|
||||||
|
}
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void pruneEmptyParents(final List<String> path) {
|
||||||
|
if (path.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final Deque<ParentLink> parents = new ArrayDeque<>();
|
||||||
|
Node current = this.root;
|
||||||
|
for (final String segment : path) {
|
||||||
|
final Node child = current.children.get(segment);
|
||||||
|
if (child == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
parents.addLast(new ParentLink(current, segment, child));
|
||||||
|
current = child;
|
||||||
|
}
|
||||||
|
while (!parents.isEmpty()) {
|
||||||
|
final ParentLink link = parents.removeLast();
|
||||||
|
if (!link.child.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
link.parent.children.remove(link.segment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record ParentLink(Node parent, String segment, Node child) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private record IndexedItem(StorageItemDescriptor descriptor, List<String> normalizedPath, String normalizedName) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Node {
|
||||||
|
private final Map<String, Node> children;
|
||||||
|
private final Map<String, IndexedItem> itemsByNormalizedName;
|
||||||
|
|
||||||
|
private Node() {
|
||||||
|
this.children = new HashMap<>();
|
||||||
|
this.itemsByNormalizedName = new HashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isEmpty() {
|
||||||
|
return this.children.isEmpty() && this.itemsByNormalizedName.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clear() {
|
||||||
|
this.children.clear();
|
||||||
|
this.itemsByNormalizedName.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package de.extio.game_engine.storage;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public record StorageItemDescriptor(UUID id, String name, List<String> path) {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
package de.extio.game_engine.storage;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
|
||||||
|
import de.extio.game_engine.util.ObjectSerialization;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
public class StorageServiceImpl {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(StorageServiceImpl.class);
|
||||||
|
|
||||||
|
private final static String INDEX_FN = "index";
|
||||||
|
|
||||||
|
private final static String DATA_DIR = "data/";
|
||||||
|
|
||||||
|
public final static Path STORAGE_LOCATION;
|
||||||
|
|
||||||
|
public final static Path DATA_LOCATION;
|
||||||
|
|
||||||
|
public final static Path INDEX_LOCATION;
|
||||||
|
|
||||||
|
static {
|
||||||
|
String location = System.getProperty("storagelocation");
|
||||||
|
if (location == null) {
|
||||||
|
try {
|
||||||
|
final URL url = StorageServiceImpl.class.getProtectionDomain().getCodeSource().getLocation();
|
||||||
|
final Path jarPath = Paths.get(url.toURI()).getParent().toAbsolutePath().normalize();
|
||||||
|
|
||||||
|
final String currentSubDir = jarPath.getFileName().toString();
|
||||||
|
if ("bin".equals(currentSubDir) || "target".equals(currentSubDir)) {
|
||||||
|
location = jarPath.getParent().resolve("storage").toString();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
location = jarPath.resolve("storage").toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (final Exception e) {
|
||||||
|
LOGGER.error("Could not determine storage location, defaulting to 'storage'", e);
|
||||||
|
location = "storage";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
STORAGE_LOCATION = Paths.get(location).toAbsolutePath().normalize();
|
||||||
|
DATA_LOCATION = STORAGE_LOCATION.resolve(DATA_DIR);
|
||||||
|
INDEX_LOCATION = STORAGE_LOCATION.resolve(INDEX_FN);
|
||||||
|
LOGGER.info("Storage location: {}", STORAGE_LOCATION);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void forceMkdir(final Path p) {
|
||||||
|
try {
|
||||||
|
Files.createDirectories(p);
|
||||||
|
}
|
||||||
|
catch (final IOException e) {
|
||||||
|
LOGGER.error("Could not create directory " + p.toString(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final StorageIndex index;
|
||||||
|
|
||||||
|
public StorageServiceImpl() {
|
||||||
|
this.index = new StorageIndex();
|
||||||
|
|
||||||
|
forceMkdir(STORAGE_LOCATION);
|
||||||
|
forceMkdir(DATA_LOCATION);
|
||||||
|
this.loadIndex();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadIndex() {
|
||||||
|
if (!Files.exists(INDEX_LOCATION)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (Files.size(INDEX_LOCATION) == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final StorageItemDescriptor[] descriptors;
|
||||||
|
try (final var in = Files.newInputStream(INDEX_LOCATION)) {
|
||||||
|
descriptors = ObjectSerialization.deserialize(StorageItemDescriptor[].class, in, false, false, null, null, null);
|
||||||
|
}
|
||||||
|
if (descriptors == null || descriptors.length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.index.clear();
|
||||||
|
for (final StorageItemDescriptor d : descriptors) {
|
||||||
|
if (d != null) {
|
||||||
|
this.index.add(d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGGER.info("Loaded {} items from index", this.index.size());
|
||||||
|
}
|
||||||
|
catch (final Exception e) {
|
||||||
|
LOGGER.error("Could not load index from " + INDEX_LOCATION.toString(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -81,13 +81,17 @@ public final class ObjectSerialization {
|
|||||||
return byteArrayOutputStream.toByteArray();
|
return byteArrayOutputStream.toByteArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static <T extends Object> T deserialize(final Class<T> clazz, final byte[] data, final boolean deserializeObjRefs, final boolean decompress, final boolean base64, final byte[] checkDigest, final byte[] digestSalt, final Consumer<Boolean> digestCheckConsumer) {
|
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);
|
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(data);
|
||||||
|
return deserialize(clazz, byteArrayInputStream, 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) {
|
||||||
InputStream base64InputStream = null;
|
InputStream base64InputStream = null;
|
||||||
ZstdInputStream zstdInputStream = null;
|
ZstdInputStream zstdInputStream = null;
|
||||||
DigestInputStream digestInputStream = null;
|
DigestInputStream digestInputStream = null;
|
||||||
try {
|
try {
|
||||||
InputStream stream = byteArrayInputStream;
|
InputStream stream = inputStream;
|
||||||
if (base64) {
|
if (base64) {
|
||||||
stream = base64InputStream = Base64.getDecoder().wrap(stream);
|
stream = base64InputStream = Base64.getDecoder().wrap(stream);
|
||||||
}
|
}
|
||||||
@@ -104,9 +108,10 @@ public final class ObjectSerialization {
|
|||||||
throw new RuntimeException(e.getMessage(), e);
|
throw new RuntimeException(e.getMessage(), e);
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
if (base64InputStream != null) {
|
if (digestInputStream != null) {
|
||||||
try {
|
try {
|
||||||
base64InputStream.close();
|
digestInputStream.close();
|
||||||
|
digestCheckConsumer.accept(Arrays.equals(checkDigest, digestInputStream.getMessageDigest().digest(digestSalt)));
|
||||||
}
|
}
|
||||||
catch (final IOException e) {
|
catch (final IOException e) {
|
||||||
throw new RuntimeException(e.getMessage(), e);
|
throw new RuntimeException(e.getMessage(), e);
|
||||||
@@ -120,10 +125,17 @@ public final class ObjectSerialization {
|
|||||||
throw new RuntimeException(e.getMessage(), e);
|
throw new RuntimeException(e.getMessage(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (digestInputStream != null) {
|
if (base64InputStream != null) {
|
||||||
try {
|
try {
|
||||||
digestInputStream.close();
|
base64InputStream.close();
|
||||||
digestCheckConsumer.accept(Arrays.equals(checkDigest, digestInputStream.getMessageDigest().digest(digestSalt)));
|
}
|
||||||
|
catch (final IOException e) {
|
||||||
|
throw new RuntimeException(e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (inputStream != null) {
|
||||||
|
try {
|
||||||
|
inputStream.close();
|
||||||
}
|
}
|
||||||
catch (final IOException e) {
|
catch (final IOException e) {
|
||||||
throw new RuntimeException(e.getMessage(), e);
|
throw new RuntimeException(e.getMessage(), e);
|
||||||
|
|||||||
137
src/test/java/de/extio/game_engine/storage/StorageIndexTest.java
Normal file
137
src/test/java/de/extio/game_engine/storage/StorageIndexTest.java
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
package de.extio.game_engine.storage;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
public class StorageIndexTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void addAndFindAtPath() {
|
||||||
|
final StorageIndex index = new StorageIndex();
|
||||||
|
final UUID id = UUID.randomUUID();
|
||||||
|
final StorageItemDescriptor descriptor = new StorageItemDescriptor(id, "File.TXT", List.of("A", "B"));
|
||||||
|
index.add(descriptor);
|
||||||
|
assertThat(index.size()).isEqualTo(1);
|
||||||
|
assertThat(index.find(List.of("a", "b"), "file.txt")).contains(descriptor);
|
||||||
|
assertThat(index.find(List.of("A", "B"), "FILE.txt")).contains(descriptor);
|
||||||
|
assertThat(index.find(List.of("a"), "file.txt")).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void listReturnsItemsOnlyAtThatDirectory() {
|
||||||
|
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"));
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void addNormalizesPathSegments() {
|
||||||
|
final StorageIndex index = new StorageIndex();
|
||||||
|
final StorageItemDescriptor descriptor = new StorageItemDescriptor(UUID.randomUUID(), "X", List.of(" ", "A", " ", "b "));
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void addRejectsNullPathSegment() {
|
||||||
|
final StorageIndex index = new StorageIndex();
|
||||||
|
final StorageItemDescriptor descriptor = new StorageItemDescriptor(UUID.randomUUID(), "x", Arrays.asList("a", null));
|
||||||
|
assertThatThrownBy(() -> index.add(descriptor)).isInstanceOf(IllegalArgumentException.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void addReplacesExistingById() {
|
||||||
|
final StorageIndex index = new StorageIndex();
|
||||||
|
final UUID id = UUID.randomUUID();
|
||||||
|
final StorageItemDescriptor first = new StorageItemDescriptor(id, "one", List.of("a"));
|
||||||
|
final StorageItemDescriptor second = new StorageItemDescriptor(id, "two", List.of("b"));
|
||||||
|
index.add(first);
|
||||||
|
index.add(second);
|
||||||
|
assertThat(index.size()).isEqualTo(1);
|
||||||
|
assertThat(index.find(List.of("a"), "one")).isEmpty();
|
||||||
|
assertThat(index.find(List.of("b"), "two")).contains(second);
|
||||||
|
assertThat(index.getById(id)).contains(second);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void addReplacesExistingByNameWithinSameDirectory() {
|
||||||
|
final StorageIndex index = new StorageIndex();
|
||||||
|
final StorageItemDescriptor first = new StorageItemDescriptor(UUID.randomUUID(), "same", List.of("a"));
|
||||||
|
final StorageItemDescriptor second = new StorageItemDescriptor(UUID.randomUUID(), "same", List.of("a"));
|
||||||
|
index.add(first);
|
||||||
|
index.add(second);
|
||||||
|
assertThat(index.size()).isEqualTo(1);
|
||||||
|
assertThat(index.find(List.of("a"), "same")).contains(second);
|
||||||
|
assertThat(index.getById(first.id())).isEmpty();
|
||||||
|
assertThat(index.getById(second.id())).contains(second);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void removeByIdRemovesAndPrunesEmptyDirectories() {
|
||||||
|
final StorageIndex index = new StorageIndex();
|
||||||
|
final StorageItemDescriptor leaf = new StorageItemDescriptor(UUID.randomUUID(), "leaf", List.of("a", "b"));
|
||||||
|
index.add(leaf);
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void removeByPathAndNameRemovesAndKeepsOtherSiblings() {
|
||||||
|
final StorageIndex index = new StorageIndex();
|
||||||
|
final StorageItemDescriptor a = new StorageItemDescriptor(UUID.randomUUID(), "a", List.of("x"));
|
||||||
|
final StorageItemDescriptor b = new StorageItemDescriptor(UUID.randomUUID(), "b", List.of("x"));
|
||||||
|
index.add(a);
|
||||||
|
index.add(b);
|
||||||
|
assertThat(index.remove(List.of("x"), "a")).isTrue();
|
||||||
|
assertThat(index.find(List.of("x"), "a")).isEmpty();
|
||||||
|
assertThat(index.find(List.of("x"), "b")).contains(b);
|
||||||
|
assertThat(index.size()).isEqualTo(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void findByFilenamePatternNonRecursive() {
|
||||||
|
final StorageIndex index = new StorageIndex();
|
||||||
|
final StorageItemDescriptor inBase1 = new StorageItemDescriptor(UUID.randomUUID(), "hero.png", List.of("assets"));
|
||||||
|
final StorageItemDescriptor inBase2 = new StorageItemDescriptor(UUID.randomUUID(), "hero.txt", List.of("assets"));
|
||||||
|
final StorageItemDescriptor inChild = new StorageItemDescriptor(UUID.randomUUID(), "hero.jpg", List.of("assets", "portraits"));
|
||||||
|
index.add(inBase1);
|
||||||
|
index.add(inBase2);
|
||||||
|
index.add(inChild);
|
||||||
|
final var matches = index.findByFilenamePattern(List.of("ASSETS"), "glob:*.png", false);
|
||||||
|
assertThat(matches).containsExactlyInAnyOrder(inBase1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void findByFilenamePatternRecursive() {
|
||||||
|
final StorageIndex index = new StorageIndex();
|
||||||
|
final StorageItemDescriptor inBase = new StorageItemDescriptor(UUID.randomUUID(), "HERO.PNG", List.of("assets"));
|
||||||
|
final StorageItemDescriptor inChild1 = new StorageItemDescriptor(UUID.randomUUID(), "hero.jpg", List.of("assets", "portraits"));
|
||||||
|
final StorageItemDescriptor inChild2 = new StorageItemDescriptor(UUID.randomUUID(), "villain.png", List.of("assets", "portraits", "alt"));
|
||||||
|
final StorageItemDescriptor outside = new StorageItemDescriptor(UUID.randomUUID(), "hero.png", List.of("other"));
|
||||||
|
index.add(inBase);
|
||||||
|
index.add(inChild1);
|
||||||
|
index.add(inChild2);
|
||||||
|
index.add(outside);
|
||||||
|
final var matches = index.findByFilenamePattern(List.of("assets"), "glob:*.png", true);
|
||||||
|
assertThat(matches).containsExactlyInAnyOrder(inBase, inChild2);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package de.extio.game_engine.storage;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.AfterAll;
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import de.extio.game_engine.util.ObjectSerialization;
|
||||||
|
|
||||||
|
class StorageServiceImplTest {
|
||||||
|
|
||||||
|
@TempDir
|
||||||
|
static Path tempDir;
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
static void setUpStorageLocation() {
|
||||||
|
System.setProperty("storagelocation", tempDir.resolve("storage").toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterAll
|
||||||
|
static void clearStorageLocation() {
|
||||||
|
System.clearProperty("storagelocation");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void loadIndexNoFileDoesNotFailAndKeepsIndexEmpty() throws Exception {
|
||||||
|
Files.deleteIfExists(StorageServiceImpl.INDEX_LOCATION);
|
||||||
|
final StorageServiceImpl service = new StorageServiceImpl();
|
||||||
|
final StorageIndex index = this.getIndex(service);
|
||||||
|
assertThat(index.isEmpty()).isTrue();
|
||||||
|
assertThat(Files.exists(StorageServiceImpl.INDEX_LOCATION)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void loadIndexDeserializesIndexFileAndAddsAllItems() throws Exception {
|
||||||
|
Files.deleteIfExists(StorageServiceImpl.INDEX_LOCATION);
|
||||||
|
Files.createDirectories(StorageServiceImpl.STORAGE_LOCATION);
|
||||||
|
|
||||||
|
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 -> {
|
||||||
|
});
|
||||||
|
Files.write(StorageServiceImpl.INDEX_LOCATION, bytes);
|
||||||
|
|
||||||
|
final StorageServiceImpl 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 {
|
||||||
|
final Field field = StorageServiceImpl.class.getDeclaredField("index");
|
||||||
|
field.setAccessible(true);
|
||||||
|
return (StorageIndex) field.get(service);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,7 +21,7 @@ public class ObjectSerializationTest {
|
|||||||
assertNotNull(serialized);
|
assertNotNull(serialized);
|
||||||
assertTrue(serialized.length > 0);
|
assertTrue(serialized.length > 0);
|
||||||
|
|
||||||
var deserialized = ObjectSerialization.deserialize(TestObject.class, serialized, false, false, false, null, null, result -> {
|
var deserialized = ObjectSerialization.deserialize(TestObject.class, serialized, false, false, null, null, result -> {
|
||||||
});
|
});
|
||||||
|
|
||||||
assertNotNull(deserialized);
|
assertNotNull(deserialized);
|
||||||
@@ -40,7 +40,7 @@ public class ObjectSerializationTest {
|
|||||||
assertNotNull(serialized);
|
assertNotNull(serialized);
|
||||||
assertTrue(serialized.length > 0);
|
assertTrue(serialized.length > 0);
|
||||||
|
|
||||||
var deserialized = ObjectSerialization.deserialize(TestObject.class, serialized, false, true, false, null, null, result -> {
|
var deserialized = ObjectSerialization.deserialize(TestObject.class, serialized, true, false, null, null, result -> {
|
||||||
});
|
});
|
||||||
|
|
||||||
assertNotNull(deserialized);
|
assertNotNull(deserialized);
|
||||||
@@ -60,7 +60,7 @@ public class ObjectSerializationTest {
|
|||||||
assertTrue(serialized.length > 0);
|
assertTrue(serialized.length > 0);
|
||||||
assertTrue(new String(serialized).matches("[A-Za-z0-9+/=]+"));
|
assertTrue(new String(serialized).matches("[A-Za-z0-9+/=]+"));
|
||||||
|
|
||||||
var deserialized = ObjectSerialization.deserialize(TestObject.class, serialized, false, false, true, null, null, result -> {
|
var deserialized = ObjectSerialization.deserialize(TestObject.class, serialized, false, true, null, null, result -> {
|
||||||
});
|
});
|
||||||
|
|
||||||
assertNotNull(deserialized);
|
assertNotNull(deserialized);
|
||||||
@@ -82,7 +82,7 @@ public class ObjectSerializationTest {
|
|||||||
assertTrue(capturedDigest[0].length == 32);
|
assertTrue(capturedDigest[0].length == 32);
|
||||||
|
|
||||||
var digestCheckPassed = new boolean[1];
|
var digestCheckPassed = new boolean[1];
|
||||||
var deserialized = ObjectSerialization.deserialize(TestObject.class, serialized, false, false, false, capturedDigest[0], digestSalt, result -> digestCheckPassed[0] = result);
|
var deserialized = ObjectSerialization.deserialize(TestObject.class, serialized, false, false, capturedDigest[0], digestSalt, result -> digestCheckPassed[0] = result);
|
||||||
|
|
||||||
assertNotNull(deserialized);
|
assertNotNull(deserialized);
|
||||||
assertTrue(digestCheckPassed[0]);
|
assertTrue(digestCheckPassed[0]);
|
||||||
@@ -102,7 +102,7 @@ public class ObjectSerializationTest {
|
|||||||
assertTrue(serialized.length > 0);
|
assertTrue(serialized.length > 0);
|
||||||
assertTrue(new String(serialized).matches("[A-Za-z0-9+/=]+"));
|
assertTrue(new String(serialized).matches("[A-Za-z0-9+/=]+"));
|
||||||
|
|
||||||
var deserialized = ObjectSerialization.deserialize(TestObject.class, serialized, false, true, true, null, null, result -> {
|
var deserialized = ObjectSerialization.deserialize(TestObject.class, serialized, true, true, null, null, result -> {
|
||||||
});
|
});
|
||||||
|
|
||||||
assertNotNull(deserialized);
|
assertNotNull(deserialized);
|
||||||
@@ -124,7 +124,7 @@ public class ObjectSerializationTest {
|
|||||||
assertTrue(new String(serialized).matches("[A-Za-z0-9+/=]+"));
|
assertTrue(new String(serialized).matches("[A-Za-z0-9+/=]+"));
|
||||||
|
|
||||||
var digestCheckPassed = new boolean[1];
|
var digestCheckPassed = new boolean[1];
|
||||||
var deserialized = ObjectSerialization.deserialize(TestObject.class, serialized, false, true, true, capturedDigest[0], digestSalt, result -> digestCheckPassed[0] = result);
|
var deserialized = ObjectSerialization.deserialize(TestObject.class, serialized, true, true, capturedDigest[0], digestSalt, result -> digestCheckPassed[0] = result);
|
||||||
|
|
||||||
assertNotNull(deserialized);
|
assertNotNull(deserialized);
|
||||||
assertTrue(digestCheckPassed[0]);
|
assertTrue(digestCheckPassed[0]);
|
||||||
|
|||||||
Reference in New Issue
Block a user