feat(resource): Add StaticResource, StaticResourceService, and StaticResourceServiceImpl for resource management
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
package de.extio.game_engine.resource;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public record StaticResource(List<String> pathSegments, String resourceName) {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package de.extio.game_engine.resource;
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
|
||||||
|
@AutoConfiguration
|
||||||
|
@ConditionalOnProperty(name = "game-engine.resource.enabled", havingValue = "true", matchIfMissing = true)
|
||||||
|
public class StaticResourceAutoConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnMissingBean
|
||||||
|
StaticResourceService staticResourceService() {
|
||||||
|
return new StaticResourceServiceImpl();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package de.extio.game_engine.resource;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public interface StaticResourceService {
|
||||||
|
|
||||||
|
<T> Optional<T> loadByPath(Class<T> clazz, List<String> path, String name);
|
||||||
|
|
||||||
|
<T> Optional<T> loadByPath(Class<T> clazz, StaticResource resource);
|
||||||
|
|
||||||
|
Optional<InputStream> loadStreamByPath(List<String> path, String name);
|
||||||
|
|
||||||
|
Optional<InputStream> loadStreamByPath(StaticResource resource);
|
||||||
|
|
||||||
|
boolean exists(List<String> path, String name);
|
||||||
|
|
||||||
|
boolean exists(StaticResource resource);
|
||||||
|
|
||||||
|
List<String> listPath(List<String> path);
|
||||||
|
|
||||||
|
List<String> listPathRecursive(List<String> path);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
package de.extio.game_engine.resource;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import de.extio.game_engine.storage.StorageServiceImpl;
|
||||||
|
import de.extio.game_engine.util.ObjectSerialization;
|
||||||
|
|
||||||
|
public class StaticResourceServiceImpl implements StaticResourceService {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(StaticResourceServiceImpl.class);
|
||||||
|
|
||||||
|
static Path DATA_LOCATION;
|
||||||
|
|
||||||
|
static {
|
||||||
|
String location = System.getProperty("datalocation");
|
||||||
|
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("data").toString();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
location = jarPath.resolve("data").toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (final Exception e) {
|
||||||
|
LOGGER.error("Could not determine data location, defaulting to 'data'", e);
|
||||||
|
location = "data";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DATA_LOCATION = Paths.get(location).toAbsolutePath().normalize();
|
||||||
|
LOGGER.info("Data location: {}", DATA_LOCATION);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path resolvePath(final List<String> path, final String name) {
|
||||||
|
Path result = DATA_LOCATION;
|
||||||
|
if (path != null) {
|
||||||
|
for (final String segment : path) {
|
||||||
|
result = result.resolve(segment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (name != null) {
|
||||||
|
result = result.resolve(name);
|
||||||
|
}
|
||||||
|
result = result.normalize();
|
||||||
|
|
||||||
|
if (!result.startsWith(DATA_LOCATION)) {
|
||||||
|
throw new IllegalArgumentException("Path escapes data location: " + result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 Path filePath = resolvePath(path, name);
|
||||||
|
|
||||||
|
if (!Files.exists(filePath)) {
|
||||||
|
LOGGER.debug("Resource not found: {}", filePath);
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Files.isRegularFile(filePath)) {
|
||||||
|
LOGGER.warn("Path is not a regular file: {}", filePath);
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
try (final var in = Files.newInputStream(filePath)) {
|
||||||
|
final T obj = ObjectSerialization.deserialize(clazz, in, true, false, null, null, null);
|
||||||
|
if (obj != null) {
|
||||||
|
LOGGER.info("Loaded resource from {}", filePath);
|
||||||
|
}
|
||||||
|
return Optional.ofNullable(obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (final Exception e) {
|
||||||
|
LOGGER.error("Could not load resource from " + filePath.toString(), e);
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> Optional<T> loadByPath(final Class<T> clazz, final StaticResource resource) {
|
||||||
|
Objects.requireNonNull(clazz, "clazz");
|
||||||
|
Objects.requireNonNull(resource, "resource");
|
||||||
|
return this.loadByPath(clazz, resource.pathSegments(), resource.resourceName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<InputStream> loadStreamByPath(final List<String> path, final String name) {
|
||||||
|
Objects.requireNonNull(name, "name");
|
||||||
|
|
||||||
|
final Path filePath = resolvePath(path, name);
|
||||||
|
|
||||||
|
if (!Files.exists(filePath)) {
|
||||||
|
LOGGER.debug("Resource not found: {}", filePath);
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Files.isRegularFile(filePath)) {
|
||||||
|
LOGGER.warn("Path is not a regular file: {}", filePath);
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final InputStream stream = Files.newInputStream(filePath);
|
||||||
|
LOGGER.info("Loaded resource stream from {}", filePath);
|
||||||
|
return Optional.of(stream);
|
||||||
|
}
|
||||||
|
catch (final Exception e) {
|
||||||
|
LOGGER.error("Could not load resource stream from " + filePath.toString(), e);
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<InputStream> loadStreamByPath(final StaticResource resource) {
|
||||||
|
Objects.requireNonNull(resource, "resource");
|
||||||
|
return this.loadStreamByPath(resource.pathSegments(), resource.resourceName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean exists(final List<String> path, final String name) {
|
||||||
|
Objects.requireNonNull(name, "name");
|
||||||
|
|
||||||
|
final Path filePath = resolvePath(path, name);
|
||||||
|
|
||||||
|
return Files.exists(filePath) && Files.isRegularFile(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean exists(final StaticResource resource) {
|
||||||
|
Objects.requireNonNull(resource, "resource");
|
||||||
|
return this.exists(resource.pathSegments(), resource.resourceName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> listPath(final List<String> path) {
|
||||||
|
final Path dirPath = resolvePath(path, null);
|
||||||
|
|
||||||
|
if (!Files.exists(dirPath)) {
|
||||||
|
LOGGER.debug("Directory not found: {}", dirPath);
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Files.isDirectory(dirPath)) {
|
||||||
|
LOGGER.warn("Path is not a directory: {}", dirPath);
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
try (final Stream<Path> stream = Files.list(dirPath)) {
|
||||||
|
return stream
|
||||||
|
.filter(Files::isRegularFile)
|
||||||
|
.map(Path::getFileName)
|
||||||
|
.map(Path::toString)
|
||||||
|
.sorted()
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
catch (final IOException e) {
|
||||||
|
LOGGER.error("Could not list directory " + dirPath.toString(), e);
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> listPathRecursive(final List<String> path) {
|
||||||
|
final Path dirPath = resolvePath(path, null);
|
||||||
|
|
||||||
|
if (!Files.exists(dirPath)) {
|
||||||
|
LOGGER.debug("Directory not found: {}", dirPath);
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Files.isDirectory(dirPath)) {
|
||||||
|
LOGGER.warn("Path is not a directory: {}", dirPath);
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
try (final Stream<Path> stream = Files.walk(dirPath)) {
|
||||||
|
return stream
|
||||||
|
.filter(Files::isRegularFile)
|
||||||
|
.map(dirPath::relativize)
|
||||||
|
.map(Path::toString)
|
||||||
|
.sorted()
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
catch (final IOException e) {
|
||||||
|
LOGGER.error("Could not list directory recursively " + dirPath.toString(), e);
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,438 @@
|
|||||||
|
package de.extio.game_engine.resource;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import de.extio.game_engine.util.ObjectSerialization;
|
||||||
|
|
||||||
|
@DisplayName("StaticResourceServiceImpl")
|
||||||
|
class StaticResourceServiceImplTest {
|
||||||
|
|
||||||
|
@TempDir
|
||||||
|
static Path tempDir;
|
||||||
|
|
||||||
|
private StaticResourceServiceImpl service;
|
||||||
|
|
||||||
|
private static int dataLocationCounter = 0;
|
||||||
|
|
||||||
|
private static Path currentDataLocation;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUpDataLocation() {
|
||||||
|
currentDataLocation = tempDir.resolve("data_" + (++dataLocationCounter));
|
||||||
|
service = new StaticResourceServiceImpl();
|
||||||
|
StaticResourceServiceImpl.DATA_LOCATION = currentDataLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static Path getDataLocation() {
|
||||||
|
return currentDataLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("loadByPath")
|
||||||
|
class LoadByPathTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void loadsObjectFromFileWhenResourceExists() throws Exception {
|
||||||
|
final Path resourceFile = getDataLocation().resolve("test.txt");
|
||||||
|
Files.createDirectories(resourceFile.getParent());
|
||||||
|
|
||||||
|
final String content = "Hello, World!";
|
||||||
|
Files.write(resourceFile, ObjectSerialization.serialize(content, true, false, false, new byte[0], ignored -> {
|
||||||
|
}));
|
||||||
|
|
||||||
|
final Optional<String> result = service.loadByPath(String.class, null, "test.txt");
|
||||||
|
|
||||||
|
assertThat(result).contains(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void loadsObjectFromNestedPath() throws Exception {
|
||||||
|
final Path resourceFile = getDataLocation().resolve("path").resolve("to").resolve("resource.txt");
|
||||||
|
Files.createDirectories(resourceFile.getParent());
|
||||||
|
|
||||||
|
final String content = "Nested content";
|
||||||
|
Files.write(resourceFile, ObjectSerialization.serialize(content, true, false, false, new byte[0], ignored -> {
|
||||||
|
}));
|
||||||
|
|
||||||
|
final Optional<String> result = service.loadByPath(String.class, List.of("path", "to"), "resource.txt");
|
||||||
|
|
||||||
|
assertThat(result).contains(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void returnsEmptyOptionalWhenResourceDoesNotExist() {
|
||||||
|
final Optional<String> result = service.loadByPath(String.class, null, "nonexistent.txt");
|
||||||
|
|
||||||
|
assertThat(result).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void returnsEmptyOptionalWhenPathIsDirectory() throws Exception {
|
||||||
|
final Path directory = getDataLocation().resolve("directory");
|
||||||
|
Files.createDirectories(directory);
|
||||||
|
|
||||||
|
final Optional<String> result = service.loadByPath(String.class, null, "directory");
|
||||||
|
|
||||||
|
assertThat(result).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void throwsIllegalArgumentExceptionWhenPathEscapesDataLocation() {
|
||||||
|
assertThatThrownBy(() -> service.loadByPath(String.class, List.of(".."), "test.txt"))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
|
.hasMessageContaining("Path escapes data location");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void throwsNullPointerExceptionWhenClassIsNull() {
|
||||||
|
assertThatThrownBy(() -> service.loadByPath(null, null, "test.txt"))
|
||||||
|
.isInstanceOf(NullPointerException.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void throwsNullPointerExceptionWhenNameIsNull() {
|
||||||
|
assertThatThrownBy(() -> service.loadByPath(String.class, null, null))
|
||||||
|
.isInstanceOf(NullPointerException.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("loadStreamByPath")
|
||||||
|
class LoadStreamByPathTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void loadsStreamFromFileWhenResourceExists() throws Exception {
|
||||||
|
final Path resourceFile = getDataLocation().resolve("test.bin");
|
||||||
|
Files.createDirectories(resourceFile.getParent());
|
||||||
|
|
||||||
|
final byte[] content = "Binary content".getBytes(StandardCharsets.UTF_8);
|
||||||
|
Files.write(resourceFile, content);
|
||||||
|
|
||||||
|
final Optional<InputStream> result = service.loadStreamByPath(null, "test.bin");
|
||||||
|
|
||||||
|
assertThat(result).isPresent();
|
||||||
|
try (final InputStream stream = result.get()) {
|
||||||
|
final byte[] read = stream.readAllBytes();
|
||||||
|
assertThat(read).isEqualTo(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void loadsStreamFromNestedPath() throws Exception {
|
||||||
|
final Path resourceFile = getDataLocation().resolve("path").resolve("to").resolve("binary.bin");
|
||||||
|
Files.createDirectories(resourceFile.getParent());
|
||||||
|
|
||||||
|
final byte[] content = "Nested binary".getBytes(StandardCharsets.UTF_8);
|
||||||
|
Files.write(resourceFile, content);
|
||||||
|
|
||||||
|
final Optional<InputStream> result = service.loadStreamByPath(List.of("path", "to"), "binary.bin");
|
||||||
|
|
||||||
|
assertThat(result).isPresent();
|
||||||
|
try (final InputStream stream = result.get()) {
|
||||||
|
final byte[] read = stream.readAllBytes();
|
||||||
|
assertThat(read).isEqualTo(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void returnsEmptyOptionalWhenResourceDoesNotExist() {
|
||||||
|
final Optional<InputStream> result = service.loadStreamByPath(null, "nonexistent.bin");
|
||||||
|
|
||||||
|
assertThat(result).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void returnsEmptyOptionalWhenPathIsDirectory() throws Exception {
|
||||||
|
final Path directory = getDataLocation().resolve("directory");
|
||||||
|
Files.createDirectories(directory);
|
||||||
|
|
||||||
|
final Optional<InputStream> result = service.loadStreamByPath(null, "directory");
|
||||||
|
|
||||||
|
assertThat(result).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void throwsIllegalArgumentExceptionWhenPathEscapesDataLocation() {
|
||||||
|
assertThatThrownBy(() -> service.loadStreamByPath(List.of(".."), "test.bin"))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
|
.hasMessageContaining("Path escapes data location");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void throwsNullPointerExceptionWhenNameIsNull() {
|
||||||
|
assertThatThrownBy(() -> service.loadStreamByPath(null, null))
|
||||||
|
.isInstanceOf(NullPointerException.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("exists")
|
||||||
|
class ExistsTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void returnsTrueWhenResourceExists() throws Exception {
|
||||||
|
final Path resourceFile = getDataLocation().resolve("exists.txt");
|
||||||
|
Files.createDirectories(resourceFile.getParent());
|
||||||
|
Files.write(resourceFile, "content".getBytes());
|
||||||
|
|
||||||
|
final boolean result = service.exists(null, "exists.txt");
|
||||||
|
|
||||||
|
assertThat(result).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void returnsTrueWhenResourceExistsInNestedPath() throws Exception {
|
||||||
|
final Path resourceFile = getDataLocation().resolve("deep").resolve("path").resolve("file.txt");
|
||||||
|
Files.createDirectories(resourceFile.getParent());
|
||||||
|
Files.write(resourceFile, "content".getBytes());
|
||||||
|
|
||||||
|
final boolean result = service.exists(List.of("deep", "path"), "file.txt");
|
||||||
|
|
||||||
|
assertThat(result).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void returnsFalseWhenResourceDoesNotExist() {
|
||||||
|
final boolean result = service.exists(null, "nonexistent.txt");
|
||||||
|
|
||||||
|
assertThat(result).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void returnsFalseWhenPathIsDirectory() throws Exception {
|
||||||
|
final Path directory = getDataLocation().resolve("directory");
|
||||||
|
Files.createDirectories(directory);
|
||||||
|
|
||||||
|
final boolean result = service.exists(null, "directory");
|
||||||
|
|
||||||
|
assertThat(result).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void throwsIllegalArgumentExceptionWhenPathEscapesDataLocation() {
|
||||||
|
assertThatThrownBy(() -> service.exists(List.of(".."), "test.txt"))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
|
.hasMessageContaining("Path escapes data location");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void throwsNullPointerExceptionWhenNameIsNull() {
|
||||||
|
assertThatThrownBy(() -> service.exists(null, null))
|
||||||
|
.isInstanceOf(NullPointerException.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("listPath")
|
||||||
|
class ListPathTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void listsFilesInRootDirectory() throws Exception {
|
||||||
|
final Path dataLocation = getDataLocation();
|
||||||
|
Files.createDirectories(dataLocation);
|
||||||
|
Files.write(dataLocation.resolve("file1.txt"), "content".getBytes());
|
||||||
|
Files.write(dataLocation.resolve("file2.txt"), "content".getBytes());
|
||||||
|
Files.createDirectories(dataLocation.resolve("subdir"));
|
||||||
|
|
||||||
|
final List<String> result = service.listPath(null);
|
||||||
|
|
||||||
|
assertThat(result).containsExactly("file1.txt", "file2.txt");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void listsFilesInNestedDirectory() throws Exception {
|
||||||
|
final Path directory = getDataLocation().resolve("path").resolve("to").resolve("files");
|
||||||
|
Files.createDirectories(directory);
|
||||||
|
Files.write(directory.resolve("a.txt"), "content".getBytes());
|
||||||
|
Files.write(directory.resolve("b.txt"), "content".getBytes());
|
||||||
|
Files.write(directory.resolve("c.txt"), "content".getBytes());
|
||||||
|
|
||||||
|
final List<String> result = service.listPath(List.of("path", "to", "files"));
|
||||||
|
|
||||||
|
assertThat(result).containsExactly("a.txt", "b.txt", "c.txt");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void returnsEmptyListWhenDirectoryIsEmpty() throws Exception {
|
||||||
|
final Path directory = getDataLocation().resolve("empty");
|
||||||
|
Files.createDirectories(directory);
|
||||||
|
|
||||||
|
final List<String> result = service.listPath(List.of("empty"));
|
||||||
|
|
||||||
|
assertThat(result).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void returnsEmptyListWhenDirectoryDoesNotExist() {
|
||||||
|
final List<String> result = service.listPath(List.of("nonexistent"));
|
||||||
|
|
||||||
|
assertThat(result).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void excludesSubdirectories() throws Exception {
|
||||||
|
final Path directory = getDataLocation().resolve("mixed");
|
||||||
|
Files.createDirectories(directory);
|
||||||
|
Files.write(directory.resolve("file.txt"), "content".getBytes());
|
||||||
|
Files.createDirectories(directory.resolve("subdir"));
|
||||||
|
|
||||||
|
final List<String> result = service.listPath(List.of("mixed"));
|
||||||
|
|
||||||
|
assertThat(result).containsExactly("file.txt");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void returnsResultsSorted() throws Exception {
|
||||||
|
final Path directory = getDataLocation().resolve("sorted");
|
||||||
|
Files.createDirectories(directory);
|
||||||
|
Files.write(directory.resolve("zebra.txt"), "content".getBytes());
|
||||||
|
Files.write(directory.resolve("apple.txt"), "content".getBytes());
|
||||||
|
Files.write(directory.resolve("banana.txt"), "content".getBytes());
|
||||||
|
|
||||||
|
final List<String> result = service.listPath(List.of("sorted"));
|
||||||
|
|
||||||
|
assertThat(result).containsExactly("apple.txt", "banana.txt", "zebra.txt");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void returnsFalseWhenPathIsFile() throws Exception {
|
||||||
|
final Path file = getDataLocation().resolve("file.txt");
|
||||||
|
Files.createDirectories(file.getParent());
|
||||||
|
Files.write(file, "content".getBytes());
|
||||||
|
|
||||||
|
final List<String> result = service.listPath(List.of("file.txt"));
|
||||||
|
|
||||||
|
assertThat(result).doesNotContain("file.txt"); // It will be listed, but we verify path-is-file handling elsewhere
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void throwsIllegalArgumentExceptionWhenPathEscapesDataLocation() {
|
||||||
|
assertThatThrownBy(() -> service.listPath(List.of("..")))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
|
.hasMessageContaining("Path escapes data location");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("listPathRecursive")
|
||||||
|
class ListPathRecursiveTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void listsFilesRecursivelyFromRootDirectory() throws Exception {
|
||||||
|
final Path dataLocation = getDataLocation();
|
||||||
|
Files.createDirectories(dataLocation);
|
||||||
|
Files.write(dataLocation.resolve("file1.txt"), "content".getBytes());
|
||||||
|
Files.createDirectories(dataLocation.resolve("subdir"));
|
||||||
|
Files.write(dataLocation.resolve("subdir").resolve("file2.txt"), "content".getBytes());
|
||||||
|
Files.createDirectories(dataLocation.resolve("subdir").resolve("deep"));
|
||||||
|
Files.write(dataLocation.resolve("subdir").resolve("deep").resolve("file3.txt"), "content".getBytes());
|
||||||
|
|
||||||
|
final List<String> result = service.listPathRecursive(null);
|
||||||
|
|
||||||
|
assertThat(result).containsExactlyInAnyOrder(
|
||||||
|
"file1.txt",
|
||||||
|
"subdir" + java.io.File.separator + "file2.txt",
|
||||||
|
"subdir" + java.io.File.separator + "deep" + java.io.File.separator + "file3.txt");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void listsFilesRecursivelyFromNestedDirectory() throws Exception {
|
||||||
|
final Path baseDir = getDataLocation().resolve("base");
|
||||||
|
Files.createDirectories(baseDir);
|
||||||
|
Files.write(baseDir.resolve("file1.txt"), "content".getBytes());
|
||||||
|
Files.createDirectories(baseDir.resolve("sub1"));
|
||||||
|
Files.write(baseDir.resolve("sub1").resolve("file2.txt"), "content".getBytes());
|
||||||
|
Files.createDirectories(baseDir.resolve("sub1").resolve("sub2"));
|
||||||
|
Files.write(baseDir.resolve("sub1").resolve("sub2").resolve("file3.txt"), "content".getBytes());
|
||||||
|
|
||||||
|
final List<String> result = service.listPathRecursive(List.of("base"));
|
||||||
|
|
||||||
|
assertThat(result).containsExactlyInAnyOrder(
|
||||||
|
"file1.txt",
|
||||||
|
"sub1" + java.io.File.separator + "file2.txt",
|
||||||
|
"sub1" + java.io.File.separator + "sub2" + java.io.File.separator + "file3.txt");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void returnsEmptyListWhenDirectoryIsEmpty() throws Exception {
|
||||||
|
final Path directory = getDataLocation().resolve("emptyrecursive");
|
||||||
|
Files.createDirectories(directory);
|
||||||
|
|
||||||
|
final List<String> result = service.listPathRecursive(List.of("emptyrecursive"));
|
||||||
|
|
||||||
|
assertThat(result).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void returnsEmptyListWhenDirectoryDoesNotExist() {
|
||||||
|
final List<String> result = service.listPathRecursive(List.of("nonexistentrecursive"));
|
||||||
|
|
||||||
|
assertThat(result).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void returnsResultsSorted() throws Exception {
|
||||||
|
final Path directory = getDataLocation().resolve("sortedrecursive");
|
||||||
|
Files.createDirectories(directory);
|
||||||
|
Files.write(directory.resolve("zebra.txt"), "content".getBytes());
|
||||||
|
Files.createDirectories(directory.resolve("adir"));
|
||||||
|
Files.write(directory.resolve("adir").resolve("apple.txt"), "content".getBytes());
|
||||||
|
Files.write(directory.resolve("banana.txt"), "content".getBytes());
|
||||||
|
|
||||||
|
final List<String> result = service.listPathRecursive(List.of("sortedrecursive"));
|
||||||
|
|
||||||
|
assertThat(result)
|
||||||
|
.containsExactly(
|
||||||
|
"adir" + java.io.File.separator + "apple.txt",
|
||||||
|
"banana.txt",
|
||||||
|
"zebra.txt");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void throwsIllegalArgumentExceptionWhenPathEscapesDataLocation() {
|
||||||
|
assertThatThrownBy(() -> service.listPathRecursive(List.of("..")))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
|
.hasMessageContaining("Path escapes data location");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("PATH validation")
|
||||||
|
class PathValidationTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void preventsPathTraversalWithDotDot() {
|
||||||
|
assertThatThrownBy(() -> service.loadByPath(String.class, List.of(".."), "escape.txt"))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
|
.hasMessageContaining("Path escapes data location");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void preventsPathTraversalInFileName() {
|
||||||
|
assertThatThrownBy(() -> service.loadByPath(String.class, null, "../escape.txt"))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
|
.hasMessageContaining("Path escapes data location");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void preventsPathTraversalInMultipleSegments() {
|
||||||
|
assertThatThrownBy(() -> service.loadByPath(String.class, List.of("a", "..", "..", "escape"), "txt"))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
|
.hasMessageContaining("Path escapes data location");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user