diff --git a/base/src/main/java/de/staropensource/engine/base/Engine.java b/base/src/main/java/de/staropensource/engine/base/Engine.java
index 7de413c..20a19c8 100644
--- a/base/src/main/java/de/staropensource/engine/base/Engine.java
+++ b/base/src/main/java/de/staropensource/engine/base/Engine.java
@@ -49,6 +49,7 @@ import org.reflections.scanners.Scanners;
import org.reflections.util.ClasspathHelper;
import org.reflections.util.ConfigurationBuilder;
+import java.io.IOException;
import java.util.*;
/**
@@ -257,7 +258,7 @@ public final class Engine extends SubsystemClass {
*
* @since v1-alpha0
*/
- private void initializeClasses() {
+ private void initializeClasses() throws IOException {
LOGGER.verb("Initializing engine classes");
// Initialize essential engine classes
diff --git a/base/src/main/java/de/staropensource/engine/base/type/FileType.java b/base/src/main/java/de/staropensource/engine/base/type/FileType.java
new file mode 100644
index 0000000..501d60d
--- /dev/null
+++ b/base/src/main/java/de/staropensource/engine/base/type/FileType.java
@@ -0,0 +1,55 @@
+/*
+ * STAROPENSOURCE ENGINE SOURCE FILE
+ * Copyright (c) 2024 The StarOpenSource Engine Authors
+ * Licensed under the GNU Affero General Public License v3
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see
+ * While this feature is built into Java, in + * our testing it did not seem to work correctly. + * That's why we're implementing it here. + * + * @since v1-alpha8 + */ + private static @NotNull Path[] scheduledDeletion = new Path[0]; + + /** + * Contains a {@link FileAccess} instance to + * a cache directory provided by the engine. + * + * @since v1-alpha8 + * -- GETTER -- + * Returns a {@link FileAccess} instance to + * a cache directory provided by the engine. + * + * @return cache directory + * @since v1-alpha8 + */ + @Getter + private static FileAccess cacheDirectory; + + // -----> Instance variables + /** + * Contains the {@link LoggerInstance} for this instance. + * + * @see LoggerInstance + * @since v1-alpha8 + */ + private final @NotNull LoggerInstance logger; + + /** + * Contains the {@link Path} to this file or directory. + * + * @since v1-alpha8 + * -- GETTER -- + * Returns the {@link Path} to this file or directory. + *
+ * Please only use this method when you have to. + * Use the methods in this class instead, if you can. + * + * @return associated {@link Path} instance + * @since v1-alpha8 + */ + private final @NotNull Path path; + + /** + * Contains the {@link File} to this file or directory. + * + * @since v1-alpha8 + * -- GETTER -- + * Returns the {@link File} to this file or directory. + *
+ * Please only use this method when you have to.
+ * Use the methods in this class instead, if you can.
+ *
+ * @return associated {@link File} instance
+ * @since v1-alpha8
+ */
+ private final @NotNull File file;
+
+ // -----> Constructors
+ /**
+ * Creates and initializes an instance of this class.
+ *
+ * @throws InvalidPathException if a {@link Path} cannot be created (see {@link FileSystem#getPath(String, String...)})
+ * @since v1-alpha8
+ */
+ public FileAccess(@NotNull String pathString) throws InvalidPathException {
+ this.path = formatPath(pathString).toAbsolutePath();
+ this.file = new File(pathString);
+ logger = new LoggerInstance.Builder().setClazz(getClass()).setOrigin("ENGINE").setMetadata(path.toString()).build();
+ }
+
+ /**
+ * Creates and initializes an instance of this class.
+ *
+ * @since v1-alpha8
+ */
+ public FileAccess(@NotNull Path path) {
+ this.path = path.toAbsolutePath();
+ this.file = this.path.toFile();
+ logger = new LoggerInstance.Builder().setClazz(getClass()).setOrigin("ENGINE").setMetadata(path.toString()).build();
+ }
+
+ /**
+ * Creates and initializes an instance of this class.
+ *
+ * @throws InvalidPathException if a {@link Path} cannot be created (see {@link FileSystem#getPath(String, String...)})
+ * @since v1-alpha8
+ */
+ public FileAccess(@NotNull File file) throws InvalidPathException {
+ this.path = file.toPath().toAbsolutePath();
+ this.file = file;
+ logger = new LoggerInstance.Builder().setClazz(getClass()).setOrigin("ENGINE").setMetadata(path.toString()).build();
+ }
+
+ /**
+ * Creates and initializes an instance of this class.
+ *
+ * @throws FileSystemNotFoundException if the filesystem is not supported by Java
+ * @throws IllegalArgumentException if the URI is improperly formatted
+ * @since v1-alpha8
+ */
+ public FileAccess(@NotNull URI uri) throws FileSystemNotFoundException, IllegalArgumentException {
+ this.path = Path.of(uri);
+ this.file = new File(uri);
+ logger = new LoggerInstance.Builder().setClazz(getClass()).setOrigin("ENGINE").setMetadata(path.toString()).build();
+ }
+
+
+ // -----> Static instance initialization
+ /**
+ * Initializes all uninitialized static
+ * {@link FileAccess} instances.
+ *
+ * @since v1-alpha8
+ */
+ public static void initializeInstances() throws IOException {
+ if (cacheDirectory == null) {
+ String temp = System.getProperty("os.name").toLowerCase(Locale.ROOT);
+
+ if (temp.contains("nix") || temp.contains("nux") || temp.contains("aix") || temp.contains("bsd"))
+ temp = "/tmp/";
+ else if (temp.contains("win"))
+ temp = System.getProperty("user.home").replace("\\", "/") + "/AppData/Local/Temp/";
+ else if (temp.contains("mac"))
+ LOGGER.crash("macOS is not supported yet");
+ else
+ LOGGER.crash("The operating system \"" + temp + "\" is not yet supported by the StarOpenSource Engine");
+
+ cacheDirectory = new FileAccess(temp + "sosengine-cache-" + ProcessHandle.current().pid()).createDirectory().deleteOnShutdown();
+ }
+ }
+ /**
+ * Deletes all files scheduled for deletion.
+ * Only works during engine shutdown.
+ *
+ * @since v1-alpha8
+ */
+ public static void deleteScheduled() {
+ if (Engine.getInstance().getState() == EngineState.SHUTDOWN || Engine.getInstance().getState() == EngineState.CRASHED) {
+ LOGGER.verb("Deleting files scheduled for deletion on shutdown");
+
+ for (Path path : scheduledDeletion)
+ try (Stream
+ * The required functionality is not yet
+ * implemented. As such, this method
+ * will just return an empty array.
+ *
+ * @return forbidden file names
+ * @since v1-alpha8
+ */
+ @ApiStatus.Experimental
+ public @NotNull String @NotNull [] getRestrictedFileNames() {
+ return new String[0];
+ }
+
+ /**
+ * Returns all forbidden characters in file names.
+ *
+ * The required functionality is not yet
+ * implemented. As such, this method
+ * will just return an empty array.
+ *
+ * @return forbidden characters
+ * @since v1-alpha8
+ */
+ @ApiStatus.Experimental
+ public char @NotNull [] getRestrictedCharacters() {
+ return new char[0];
+ }
+
+
+ // -----> Directory traversal
+ /**
+ * Returns the parent directory.
+ *
+ * @return new {@link FileAccess} instance to the parent directory
+ * @since v1-alpha8
+ */
+ public @NotNull FileAccess parent() {
+ return new FileAccess(path.getParent());
+ }
+
+ /**
+ * Traverses through directories and files.
+ *
+ * @param path path to traverse
+ * @return new {@link FileAccess} instance
+ * @since v1-alpha8
+ */
+ public @NotNull FileAccess traverse(@NotNull String path) {
+ return new FileAccess(this.path.resolve(formatPath(path)));
+ }
+
+ /**
+ * Traverses through directories and files.
+ *
+ * @param path path to traverse
+ * @return new {@link FileAccess} instance
+ * @throws FileNotFoundException if the specified path does not exist
+ * @since v1-alpha8
+ */
+ public @NotNull FileAccess traverseIfExists(@NotNull String path) throws FileNotFoundException {
+ Path pathResolved = this.path.resolve(formatPath(path));
+
+ if (!Files.exists(pathResolved))
+ throw new FileNotFoundException("Traversal failed as relative path \"" + path + "\" does not exist from absolute path \"" + path + "\"");
+
+ return new FileAccess(pathResolved);
+ }
+
+
+ // -----> File/Directory creation and deletion
+ /**
+ * Deletes the file.
+ * If it doesn't exist, nothing will be done.
+ *
+ * @return this instance
+ * @since v1-alpha8
+ */
+ public @NotNull FileAccess delete() {
+ if (exists()) {
+ logger.diag("Deleting \"" + path + "\"");
+ //noinspection ResultOfMethodCallIgnored
+ file.delete();
+ }
+ return this;
+ }
+
+ /**
+ * Marks this file for deletion at engine shutdown.
+ *
+ * @return this instance
+ * @see Engine#shutdown()
+ * @see Engine#shutdown(int)
+ * @since v1-alpha8
+ */
+ public @NotNull FileAccess deleteOnShutdown() {
+ logger.diag("Marking \"" + path + "\" for deletion at engine shutdown");
+
+ // Append path to scheduledDeletion array
+ List<@NotNull Path> scheduledDeletionList = new ArrayList<>(Arrays.stream(scheduledDeletion).toList());
+ scheduledDeletionList.add(path);
+ scheduledDeletion = scheduledDeletionList.toArray(new Path[0]);
+
+ return this;
+ }
+
+ /**
+ * Creates the file.
+ * If it already exists, nothing will be done.
+ *
+ * @return this instance
+ * @throws IOException on an IO error
+ * @since v1-alpha8
+ */
+ @SuppressWarnings({"UnusedReturnValue", "ResultOfMethodCallIgnored"})
+ public @NotNull FileAccess createFile() throws IOException {
+ if (!exists()) {
+ logger.diag("Creating a file at \"" + path + "\"");
+ file.getParentFile().mkdirs();
+ file.createNewFile();
+ }
+
+ return this;
+ }
+
+ /**
+ * Creates the directory recursively.
+ * If it already exists, nothing will be done.
+ *
+ * @return this instance
+ * @throws IOException on an IO error
+ * @since v1-alpha8
+ */
+ public @NotNull FileAccess createDirectory() throws IOException {
+ if (!exists()) {
+ logger.diag("Creating a directory at \"" + path + "\"");
+ if (!file.mkdirs())
+ throw new IOException("Creating directory \"" + path + "\" recursively failed");
+ }
+
+ return this;
+ }
+
+ /**
+ * Creates a symbolic link at this location.
+ * If it already exists, nothing will be done.
+ *
+ * @param hard creates a hard link if {@code true} or a symbolic link if {@code false}
+ * @return this instance
+ * @throws IOException on an IO error
+ * @since v1-alpha8
+ */
+ @SuppressWarnings("UnusedReturnValue")
+ public @NotNull FileAccess createLink(boolean hard, @NotNull String destination) throws IOException {
+ if (!exists()) {
+ logger.diag("Creating a " + (hard ? "hard" : "symbolic") + " link at \"" + path + "\"");
+
+ if (hard)
+ Files.createLink(path, formatPath(destination));
+ else
+ Files.createSymbolicLink(path, formatPath(destination));
+ }
+
+ return this;
+ }
+
+
+ // -----> File locking
+ /**
+ * Returns whether or not this file is locked.
+ *
+ * @return is locked?
+ * @since v1-alpha8
+ */
+ public boolean isLocked() {
+ return false;
+ }
+
+ /**
+ * Locks this file.
+ *
+ * @return this instance
+ * @since v1-alpha8
+ */
+ public @NotNull FileAccess lock() {
+ return this;
+ }
+
+ /**
+ * Unlocks this file.
+ *
+ * @return this instance
+ * @since v1-alpha8
+ */
+ public @NotNull FileAccess unlock() {
+ return this;
+ }
+
+
+ // -----> Content reading
+ /**
+ * Returns the contents of this file.
+ *
+ * Returns an empty array if this file
+ * is not of type {@link FileType#FILE}.
+ *
+ * @return file contents in bytes
+ * @throws IOException on an IO error
+ * @throws OutOfMemoryError if the file is larger than the allocated amount of memory
+ * @since v1-alpha8
+ */
+ public byte @NotNull [] readBytes() throws IOException, OutOfMemoryError {
+ if (getType() != FileType.FILE)
+ return new byte[0];
+
+ logger.diag("Reading file \"" + path + "\" (bytes)");
+ return Files.readAllBytes(path);
+ }
+
+ /**
+ * Returns the contents of this file.
+ *
+ * Returns an empty list if this file
+ * is not of type {@link FileType#FILE}.
+ *
+ * @return file contents in bytes
+ * @throws IOException on an IO error
+ * @throws OutOfMemoryError if the file is larger than the allocated amount of memory
+ * @since v1-alpha8
+ */
+ public @NotNull List<@NotNull String> readLines() throws IOException, OutOfMemoryError {
+ if (getType() != FileType.FILE)
+ return new ArrayList<>();
+
+ logger.diag("Reading file \"" + path + "\" (lines)");
+ return Files.readAllLines(path);
+ }
+
+ /**
+ * Returns the contents of this file.
+ * This method will decode the bytes using the
+ * {@link StandardCharsets#UTF_8} character set.
+ *
+ * Returns an empty string if this file
+ * is not of type {@link FileType#FILE}.
+ *
+ * @return file contents as a string
+ * @throws IOException on an IO error
+ * @throws OutOfMemoryError if the file is larger than the allocated amount of memory
+ * @since v1-alpha8
+ */
+ public @NotNull String readContent() throws IOException, OutOfMemoryError {
+ return readContent(StandardCharsets.UTF_8);
+ }
+
+ /**
+ * Returns the contents of this file.
+ *
+ * Returns an empty string if this file
+ * is not of type {@link FileType#FILE}.
+ *
+ * @param charset charset to decode the bytes with
+ * @return file contents as a string
+ * @throws IOException on an IO error
+ * @throws OutOfMemoryError if the file is larger than the allocated amount of memory
+ * @since v1-alpha8
+ */
+ public @NotNull String readContent(@NotNull Charset charset) throws IOException, OutOfMemoryError {
+ if (getType() != FileType.FILE)
+ return "";
+
+ logger.diag("Reading file \"" + path + "\" (string)");
+ return Files.readString(path, charset);
+ }
+
+
+ // -----> Content writing
+ /**
+ * Writes the specified bytes into this file.
+ *
+ * @param bytes bytes to write
+ * @param async allows the operating system to decide when to flush the file to disk if {@code true}, flushes the data to disk immediately if {@code false}
+ * @throws UnsupportedOperationException if the type of this file is neither {@link FileType#VOID} or {@link FileType#FILE}
+ * @throws IOException on an IO error
+ * @return this instance
+ */
+ public @NotNull FileAccess writeBytes(byte @NotNull [] bytes, boolean async) throws UnsupportedOperationException, IOException {
+ if (getType() == FileType.VOID)
+ createFile();
+ else if (getType() != FileType.FILE)
+ throw new UnsupportedOperationException("File \"" + path + "\" is not of type FileType.VOID or FileType.FILE");
+
+ createFile();
+ logger.diag("Writing file \"" + path + "\" (bytes, " + (async ? "async" : "dsync") + ")");
+ Files.write(path, bytes, StandardOpenOption.WRITE, async ? StandardOpenOption.DSYNC : StandardOpenOption.SYNC);
+ return this;
+ }
+
+ /**
+ * Writes the specified bytes into this file.
+ * This method will encode the string using the
+ * {@link StandardCharsets#UTF_8} character set.
+ *
+ * @param string string to write
+ * @param async allows the operating system to decide when to flush the file to disk if {@code true}, flushes the data to disk immediately if {@code false}
+ * @throws UnsupportedOperationException if the type of this file is neither {@link FileType#VOID} or {@link FileType#FILE}
+ * @throws IOException on an IO error
+ * @return this instance
+ */
+ public @NotNull FileAccess writeString(@NotNull String string, boolean async) throws UnsupportedOperationException, IOException {
+ return writeString(string, StandardCharsets.UTF_8, async);
+ }
+
+ /**
+ * Writes the specified bytes into this file.
+ *
+ * @param string string to write
+ * @param charset charset to encode the string in
+ * @param async allows the operating system to decide when to flush the file to disk if {@code true}, flushes the data to disk immediately if {@code false}
+ * @throws UnsupportedOperationException if the type of this file is neither {@link FileType#VOID} or {@link FileType#FILE}
+ * @throws IOException on an IO error
+ * @return this instance
+ */
+ public @NotNull FileAccess writeString(@NotNull String string, @NotNull Charset charset, boolean async) throws UnsupportedOperationException, IOException {
+ if (getType() == FileType.VOID)
+ createFile();
+ else if (getType() != FileType.FILE)
+ throw new UnsupportedOperationException("File \"" + path + "\" is not of type FileType.VOID or FileType.FILE");
+
+ logger.diag("Writing file \"" + path + "\" (string, " + (async ? "async" : "dsync") + ")");
+ Files.writeString(path, string, charset, StandardOpenOption.WRITE, async ? StandardOpenOption.DSYNC : StandardOpenOption.SYNC);
+ return this;
+ }
+
+
+ // -----> Content appending
+ /**
+ * Appends the specified bytes to this file.
+ *
+ * @param bytes bytes to append
+ * @param async allows the operating system to decide when to flush the file to disk if {@code true}, flushes the data to disk immediately if {@code false}
+ * @throws UnsupportedOperationException if the type of this file is neither {@link FileType#VOID} or {@link FileType#FILE}
+ * @throws IOException on an IO error
+ * @return this instance
+ */
+ public @NotNull FileAccess appendBytes(byte @NotNull [] bytes, boolean async) throws UnsupportedOperationException, IOException {
+ if (getType() == FileType.VOID)
+ createFile();
+ else if (getType() != FileType.FILE)
+ throw new UnsupportedOperationException("File \"" + path + "\" is not of type FileType.VOID or FileType.FILE");
+
+ logger.diag("Appending file \"" + path + "\" (bytes, " + (async ? "async" : "dsync") + ")");
+ Files.write(path, bytes, StandardOpenOption.APPEND, async ? StandardOpenOption.DSYNC : StandardOpenOption.SYNC);
+ return this;
+ }
+
+ /**
+ * Appends the specified string to this file.
+ * This method will encode the string using the
+ * {@link StandardCharsets#UTF_8} character set.
+ *
+ * @param string string to append
+ * @param async allows the operating system to decide when to flush the file to disk if {@code true}, flushes the data to disk immediately if {@code false}
+ * @throws UnsupportedOperationException if the type of this file is neither {@link FileType#VOID} or {@link FileType#FILE}
+ * @throws IOException on an IO error
+ * @return this instance
+ */
+ public @NotNull FileAccess appendString(@NotNull String string, boolean async) throws UnsupportedOperationException, IOException {
+ return appendString(string, StandardCharsets.UTF_8, async);
+ }
+
+ /**
+ * Appends the specified string to this file.
+ *
+ * @param string string to append
+ * @param charset charset to encode the string in
+ * @param async allows the operating system to decide when to flush the file to disk if {@code true}, flushes the data to disk immediately if {@code false}
+ * @throws UnsupportedOperationException if the type of this file is neither {@link FileType#VOID} or {@link FileType#FILE}
+ * @throws IOException on an IO error
+ * @return this instance
+ */
+ public @NotNull FileAccess appendString(@NotNull String string, @NotNull Charset charset, boolean async) throws UnsupportedOperationException, IOException {
+ if (getType() == FileType.VOID)
+ createFile();
+ else if (getType() != FileType.FILE)
+ throw new UnsupportedOperationException("File \"" + path + "\" is not of type FileType.VOID or FileType.FILE");
+
+ logger.diag("Appending file \"" + path + "\" (string, " + (async ? "async" : "dsync") + ")");
+ Files.writeString(path, string, charset, StandardOpenOption.APPEND, async ? StandardOpenOption.DSYNC : StandardOpenOption.SYNC);
+ return this;
+ }
+}