diff --git a/common/src/main/java/org/polyfrost/oneconfig/loader/IMetaHolder.java b/common/src/main/java/org/polyfrost/oneconfig/loader/IMetaHolder.java
index 6b3b950..c707a1f 100644
--- a/common/src/main/java/org/polyfrost/oneconfig/loader/IMetaHolder.java
+++ b/common/src/main/java/org/polyfrost/oneconfig/loader/IMetaHolder.java
@@ -1,11 +1,23 @@
package org.polyfrost.oneconfig.loader;
/**
- * A loader metadata container trait.
+ * A loader metadata trait, describing information about a specific loader and/or loader stage.
+ *
+ * Note that this is regarding OneConfig Loader's metadata, not an underlying game modloader.
*
* @author xtrm
*/
public interface IMetaHolder {
+ /**
+ * @return the loader's name
+ */
+ String getName();
+
+ /**
+ * @return the loader's version
+ */
+ String getVersion();
+
/**
* Creates a {@link IMetaHolder} instance from the given params.
*
@@ -26,18 +38,4 @@ public String getVersion() {
}
};
}
-
- /**
- * Returns the loader's name.
- *
- * @return the loader's name
- */
- String getName();
-
- /**
- * Returns the loader's version.
- *
- * @return the loader's version
- */
- String getVersion();
}
diff --git a/stage1/build.gradle.kts b/stage1/build.gradle.kts
index 38940e7..9ce0b16 100644
--- a/stage1/build.gradle.kts
+++ b/stage1/build.gradle.kts
@@ -11,5 +11,6 @@ sourceSets {
dependencies {
implementation(project(":common"))
+ include("cc.polyfrost:polyio:0.0.13")
"legacyCompileOnly"("net.minecraft:launchwrapper:1.12")
}
\ No newline at end of file
diff --git a/stage1/src/legacy/java/org/polyfrost/oneconfig/loader/stage1/LegacyCapabilities.java b/stage1/src/legacy/java/org/polyfrost/oneconfig/loader/stage1/LegacyCapabilities.java
index c119684..966e09b 100644
--- a/stage1/src/legacy/java/org/polyfrost/oneconfig/loader/stage1/LegacyCapabilities.java
+++ b/stage1/src/legacy/java/org/polyfrost/oneconfig/loader/stage1/LegacyCapabilities.java
@@ -11,7 +11,8 @@
import java.nio.file.Path;
/**
- * {@link Capabilities} implementation for initialization through legacy entrypoints.
+ * {@link Capabilities} implementation for initialization through legacy entrypoints, which all use
+ * LaunchWrapper.
*
* @author xtrm
* @since 1.1.0
diff --git a/stage1/src/main/java/org/polyfrost/oneconfig/loader/stage1/Stage1Loader.java b/stage1/src/main/java/org/polyfrost/oneconfig/loader/stage1/Stage1Loader.java
index 4bcb431..befe8db 100644
--- a/stage1/src/main/java/org/polyfrost/oneconfig/loader/stage1/Stage1Loader.java
+++ b/stage1/src/main/java/org/polyfrost/oneconfig/loader/stage1/Stage1Loader.java
@@ -1,23 +1,39 @@
package org.polyfrost.oneconfig.loader.stage1;
+import lombok.SneakyThrows;
import org.polyfrost.oneconfig.loader.LoaderBase;
+import org.polyfrost.oneconfig.loader.stage1.dependency.DependencyManager;
+import org.polyfrost.oneconfig.loader.stage1.dependency.maven.MavenDependencyManager;
+import org.polyfrost.oneconfig.loader.stage1.dependency.model.Artifact;
+import org.polyfrost.oneconfig.loader.stage1.util.SystemProperties;
+import org.polyfrost.oneconfig.loader.stage1.util.XDG;
/**
* @author xtrm
* @since 1.1.0
*/
public class Stage1Loader extends LoaderBase {
+ private final XDG.ApplicationStore applicationStore;
+ private final DependencyManager dependencyManager;
+
+ @SneakyThrows
public Stage1Loader(Capabilities capabilities) {
super(
"stage1",
Stage1Loader.class.getPackage().getImplementationVersion(),
capabilities
);
+ this.applicationStore = XDG.provideApplicationStore("Polyfrost/OneConfig/loader");
+ this.dependencyManager = new MavenDependencyManager(
+ applicationStore,
+ SystemProperties.REPOSITORY_URL.get()
+ );
}
@Override
public void load() {
// Fetch oneconfig version info
+ Artifact oneconfigArtifact = dependencyManager.fetchArtifact();
// Lookup dependencies metadata
// Download to cache
// Delegate everything to OneConfig
diff --git a/stage1/src/main/java/org/polyfrost/oneconfig/loader/stage1/dependency/DependencyManager.java b/stage1/src/main/java/org/polyfrost/oneconfig/loader/stage1/dependency/DependencyManager.java
new file mode 100644
index 0000000..4d7eac3
--- /dev/null
+++ b/stage1/src/main/java/org/polyfrost/oneconfig/loader/stage1/dependency/DependencyManager.java
@@ -0,0 +1,16 @@
+package org.polyfrost.oneconfig.loader.stage1.dependency;
+
+import org.polyfrost.oneconfig.loader.stage1.dependency.model.Artifact;
+import org.polyfrost.oneconfig.loader.stage1.dependency.model.ArtifactDeclaration;
+
+/**
+ *
+ * @param
+ *
+ * @author xtrm
+ * @since 1.1.0
+ */
+public interface DependencyManager {
+ A buildArtifact(String groupId, String artifactId, String version);
+ D resolveArtifact(A artifact);
+}
diff --git a/stage1/src/main/java/org/polyfrost/oneconfig/loader/stage1/dependency/DependencyResolver.java b/stage1/src/main/java/org/polyfrost/oneconfig/loader/stage1/dependency/DependencyResolver.java
deleted file mode 100644
index 21c7d78..0000000
--- a/stage1/src/main/java/org/polyfrost/oneconfig/loader/stage1/dependency/DependencyResolver.java
+++ /dev/null
@@ -1,8 +0,0 @@
-package org.polyfrost.oneconfig.loader.stage1.dependency;
-
-/**
- * @author xtrm
- * @since 1.1.0
- */
-public class DependencyResolver {
-}
diff --git a/stage1/src/main/java/org/polyfrost/oneconfig/loader/stage1/dependency/cache/CachingSolution.java b/stage1/src/main/java/org/polyfrost/oneconfig/loader/stage1/dependency/cache/CachingSolution.java
new file mode 100644
index 0000000..3039d45
--- /dev/null
+++ b/stage1/src/main/java/org/polyfrost/oneconfig/loader/stage1/dependency/cache/CachingSolution.java
@@ -0,0 +1,11 @@
+package org.polyfrost.oneconfig.loader.stage1.dependency.cache;
+
+import java.nio.file.Path;
+
+/**
+ * @author xtrm
+ * @since 1.1.0
+ */
+public interface CachingSolution {
+ boolean canBeCached(Path path);
+}
diff --git a/stage1/src/main/java/org/polyfrost/oneconfig/loader/stage1/dependency/maven/MavenArtifact.java b/stage1/src/main/java/org/polyfrost/oneconfig/loader/stage1/dependency/maven/MavenArtifact.java
new file mode 100644
index 0000000..b666171
--- /dev/null
+++ b/stage1/src/main/java/org/polyfrost/oneconfig/loader/stage1/dependency/maven/MavenArtifact.java
@@ -0,0 +1,45 @@
+package org.polyfrost.oneconfig.loader.stage1.dependency.maven;
+
+import lombok.Data;
+import org.polyfrost.oneconfig.loader.stage1.dependency.model.Artifact;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+/**
+ * @author xtrm
+ * @since 1.1.0
+ */
+public @Data class MavenArtifact implements Artifact {
+ private final String groupId;
+ private final String artifactId;
+ private final String version;
+ private final String classifier;
+ private final String extension;
+
+ @Override
+ public String getDeclaration() {
+ return String.format(
+ "%s:%s:%s",
+ groupId, artifactId, version
+ ) + (classifier == null ? "" : ":" + classifier)
+ + (extension == null ? "" : "@" + extension);
+ }
+
+ @Override
+ public Path getRelativePath() {
+ return Paths.get(
+ groupId.replace('.', '/'),
+ artifactId,
+ version,
+ getFileName()
+ );
+ }
+
+ @Override
+ public String getFileName() {
+ return artifactId + "-" + version
+ + (classifier == null ? "" : "-" + classifier)
+ + "." + (extension == null ? "jar" : extension);
+ }
+}
diff --git a/stage1/src/main/java/org/polyfrost/oneconfig/loader/stage1/dependency/maven/MavenArtifactDeclaration.java b/stage1/src/main/java/org/polyfrost/oneconfig/loader/stage1/dependency/maven/MavenArtifactDeclaration.java
new file mode 100644
index 0000000..babfce7
--- /dev/null
+++ b/stage1/src/main/java/org/polyfrost/oneconfig/loader/stage1/dependency/maven/MavenArtifactDeclaration.java
@@ -0,0 +1,30 @@
+package org.polyfrost.oneconfig.loader.stage1.dependency.maven;
+
+import lombok.Data;
+import org.polyfrost.oneconfig.loader.stage1.dependency.DependencyManager;
+import org.polyfrost.oneconfig.loader.stage1.dependency.model.Artifact;
+import org.polyfrost.oneconfig.loader.stage1.dependency.model.ArtifactDeclaration;
+import org.polyfrost.oneconfig.loader.stage1.dependency.model.ArtifactDependency;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author xtrm
+ * @since 1.1.0
+ */
+public @Data class MavenArtifactDeclaration implements ArtifactDeclaration {
+ private final MavenArtifact artifact;
+ private final List dependencies = new ArrayList<>();
+
+ void resolveDependencies(DependencyManager dependencyManager) {
+ for (MavenArtifactDependency dependency : dependencies) {
+ MavenArtifactDeclaration declaration = dependency.getDeclaration();
+ if (declaration == null) {
+ declaration = (MavenArtifactDeclaration) dependencyManager.resolveArtifact(dependency.getDeclaration().getArtifact());
+ dependency.setDeclaration(declaration);
+ declaration.resolveDependencies(dependencyManager);
+ }
+ }
+ }
+}
diff --git a/stage1/src/main/java/org/polyfrost/oneconfig/loader/stage1/dependency/maven/MavenDependencyManager.java b/stage1/src/main/java/org/polyfrost/oneconfig/loader/stage1/dependency/maven/MavenDependencyManager.java
new file mode 100644
index 0000000..04650ad
--- /dev/null
+++ b/stage1/src/main/java/org/polyfrost/oneconfig/loader/stage1/dependency/maven/MavenDependencyManager.java
@@ -0,0 +1,48 @@
+package org.polyfrost.oneconfig.loader.stage1.dependency.maven;
+
+import lombok.Getter;
+import org.polyfrost.oneconfig.loader.stage1.dependency.DependencyManager;
+import org.polyfrost.oneconfig.loader.stage1.dependency.cache.CachingSolution;
+import org.polyfrost.oneconfig.loader.stage1.dependency.maven.cache.MavenCachingSolution;
+import org.polyfrost.oneconfig.loader.stage1.util.XDG;
+
+import java.net.URI;
+import java.nio.file.Path;
+
+/**
+ * @author xtrm
+ * @since 1.1.0
+ */
+@Getter
+public class MavenDependencyManager implements DependencyManager {
+ private final XDG.ApplicationStore store;
+ private final URI repository;
+ private final CachingSolution cache;
+
+ public MavenDependencyManager(XDG.ApplicationStore store, URI repository) {
+ this.store = store;
+ this.repository = repository;
+ this.cache = new MavenCachingSolution(store, repository);
+ }
+
+ @Override
+ public MavenArtifact buildArtifact(String groupId, String artifactId, String version) {
+ return new MavenArtifact(groupId, artifactId, version, null, "jar");
+ }
+
+ @Override
+ public MavenArtifactDeclaration resolveArtifact(MavenArtifact artifact) {
+ Path dataDir = store.getDataDir();
+ Path localLibraries = dataDir.resolve("libraries");
+
+ Path artifactRelativePath = artifact.getRelativePath();
+ Path localArtifactPath = localLibraries.resolve(artifactRelativePath);
+
+ Path localPomFile = localLibraries.resolve(artifactRelativePath.toString().replace(artifact.getExtension(), "pom"));
+ Path remotePomFile = repository
+ if (localPomFile.toFile().exists()) {
+ return new MavenArtifactDeclaration(artifact);
+ }
+ return null;
+ }
+}
diff --git a/stage1/src/main/java/org/polyfrost/oneconfig/loader/stage1/dependency/maven/cache/MavenCachingSolution.java b/stage1/src/main/java/org/polyfrost/oneconfig/loader/stage1/dependency/maven/cache/MavenCachingSolution.java
new file mode 100644
index 0000000..1d01e8f
--- /dev/null
+++ b/stage1/src/main/java/org/polyfrost/oneconfig/loader/stage1/dependency/maven/cache/MavenCachingSolution.java
@@ -0,0 +1,66 @@
+package org.polyfrost.oneconfig.loader.stage1.dependency.maven.cache;
+
+import lombok.RequiredArgsConstructor;
+import org.polyfrost.oneconfig.loader.stage1.dependency.cache.CachingSolution;
+import org.polyfrost.oneconfig.loader.stage1.util.XDG;
+import org.polyfrost.oneconfig.loader.utils.RequestHelper;
+
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.net.URL;
+import java.net.URLConnection;
+import java.nio.file.Path;
+
+/**
+ * @author xtrm
+ * @since 1.1.0
+ */
+@RequiredArgsConstructor
+public class MavenCachingSolution extends RequestHelper implements CachingSolution {
+ private static final String[] CHECKSUM_EXT = new String[]{"sha512", "sha256", "sha1", "md5"};
+ private final XDG.ApplicationStore store;
+ private final URI remoteUrl;
+
+ @Override
+ public boolean canBeCached(Path path) {
+ String fileName = path.getFileName().toString();
+ if (!fileName.endsWith(".pom") && !fileName.endsWith(".jar")) {
+ return false;
+ }
+ // check on the remote server if any signature file exist
+ for (String checksumExtension : CHECKSUM_EXT) {
+ Path checksumPath = path.resolveSibling(fileName + "." + checksumExtension);
+ URI checksumUri = remoteUrl.resolve(checksumPath.toString());
+ // if the checksum file exists, then the file can be cached
+ URL url;
+ try {
+ url = checksumUri.toURL();
+ } catch (Exception e) {
+ continue;
+ }
+ if (urlExists(url)) {
+ System.out.println("File " + path + " can be cached (found " + url + ")");
+ return true;
+ }
+ }
+ System.out.println("File " + path + " cannot be cached");
+ return false;
+ }
+
+ private boolean urlExists(URL url) {
+ try {
+ URLConnection connection = establishConnection(url);
+ connection.connect();
+ long contentLength = connection.getContentLengthLong();
+ int responseCode = 200;
+ if (connection instanceof HttpURLConnection) {
+ responseCode = ((HttpURLConnection) connection).getResponseCode();
+ }
+ connection.getInputStream().close();
+ connection.getOutputStream().close();
+ return (responseCode >= 200 && responseCode < 300) && contentLength > 0;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+}
diff --git a/stage1/src/main/java/org/polyfrost/oneconfig/loader/stage1/dependency/model/Artifact.java b/stage1/src/main/java/org/polyfrost/oneconfig/loader/stage1/dependency/model/Artifact.java
new file mode 100644
index 0000000..3afdcbc
--- /dev/null
+++ b/stage1/src/main/java/org/polyfrost/oneconfig/loader/stage1/dependency/model/Artifact.java
@@ -0,0 +1,53 @@
+package org.polyfrost.oneconfig.loader.stage1.dependency.model;
+
+import org.jetbrains.annotations.Nullable;
+
+import java.nio.file.Path;
+
+/**
+ * A generic artifact, part of the dependency resolution system.
+ *
+ * @author xtrm
+ * @since 1.1.0
+ */
+public interface Artifact {
+ /**
+ * @return the artifact's group ID
+ */
+ String getGroupId();
+
+ /**
+ * @return the artifact's name/ID
+ */
+ String getArtifactId();
+
+ /**
+ * @return the artifact's version
+ */
+ String getVersion();
+
+ /**
+ * @return the artifact's classifier (usually {@code null})
+ */
+ @Nullable String getClassifier();
+
+ /**
+ * @return the artifact's extension (usually {@code jar})
+ */
+ String getExtension();
+
+ /**
+ * @return a service-specific declaration of the artifact
+ */
+ String getDeclaration();
+
+ /**
+ * @return the relative path of the artifact
+ */
+ Path getRelativePath();
+
+ /**
+ * @return the file name of the artifact
+ */
+ String getFileName();
+}
diff --git a/stage1/src/main/java/org/polyfrost/oneconfig/loader/stage1/dependency/model/ArtifactDeclaration.java b/stage1/src/main/java/org/polyfrost/oneconfig/loader/stage1/dependency/model/ArtifactDeclaration.java
new file mode 100644
index 0000000..acae417
--- /dev/null
+++ b/stage1/src/main/java/org/polyfrost/oneconfig/loader/stage1/dependency/model/ArtifactDeclaration.java
@@ -0,0 +1,15 @@
+package org.polyfrost.oneconfig.loader.stage1.dependency.model;
+
+import java.util.List;
+
+/**
+ * A resolved declaration of an artifact, with its hard-referenced dependencies and other properties.
+ *
+ * @author xtrm
+ * @since 1.1.0
+ */
+public interface ArtifactDeclaration {
+ Artifact getArtifact();
+
+ List getDependencies();
+}
diff --git a/stage1/src/main/java/org/polyfrost/oneconfig/loader/stage1/dependency/model/ArtifactDependency.java b/stage1/src/main/java/org/polyfrost/oneconfig/loader/stage1/dependency/model/ArtifactDependency.java
new file mode 100644
index 0000000..cc9a4da
--- /dev/null
+++ b/stage1/src/main/java/org/polyfrost/oneconfig/loader/stage1/dependency/model/ArtifactDependency.java
@@ -0,0 +1,15 @@
+package org.polyfrost.oneconfig.loader.stage1.dependency.model;
+
+import java.util.List;
+
+/**
+ * @author xtrm
+ * @since 1.1.0
+ */
+public interface ArtifactDependency {
+ ArtifactDeclaration getDeclaration();
+
+ Scope getScope();
+
+ List getExclusions();
+}
diff --git a/stage1/src/main/java/org/polyfrost/oneconfig/loader/stage1/dependency/model/DependencyExclusion.java b/stage1/src/main/java/org/polyfrost/oneconfig/loader/stage1/dependency/model/DependencyExclusion.java
new file mode 100644
index 0000000..77e8ffe
--- /dev/null
+++ b/stage1/src/main/java/org/polyfrost/oneconfig/loader/stage1/dependency/model/DependencyExclusion.java
@@ -0,0 +1,26 @@
+package org.polyfrost.oneconfig.loader.stage1.dependency.model;
+
+/**
+ * @author xtrm
+ * @since 1.1.0
+ */
+public interface DependencyExclusion {
+ /**
+ * @return the group being excluded, null if not specified, or a {@code *} wildcard
+ */
+ String getGroupId();
+
+ /**
+ * @return the artifact id being excluded, null if not specified, or a {@code *} wildcard
+ */
+ String getArtifactId();
+
+ /**
+ * Performs a match against the given artifact.
+ *
+ * @param artifact the artifact to match against
+ *
+ * @return {@code true} if the artifact matches the exclusion, {@code false} otherwise
+ */
+ boolean matches(Artifact artifact);
+}
diff --git a/stage1/src/main/java/org/polyfrost/oneconfig/loader/stage1/dependency/model/RemoteResolver.java b/stage1/src/main/java/org/polyfrost/oneconfig/loader/stage1/dependency/model/RemoteResolver.java
new file mode 100644
index 0000000..fa0a760
--- /dev/null
+++ b/stage1/src/main/java/org/polyfrost/oneconfig/loader/stage1/dependency/model/RemoteResolver.java
@@ -0,0 +1,11 @@
+package org.polyfrost.oneconfig.loader.stage1.dependency.model;
+
+import java.net.URI;
+
+/**
+ * @author xtrm
+ * @since 1.1.0
+ */
+public interface RemoteResolver {
+ URI resolve(Artifact artifact);
+}
diff --git a/stage1/src/main/java/org/polyfrost/oneconfig/loader/stage1/dependency/model/Scope.java b/stage1/src/main/java/org/polyfrost/oneconfig/loader/stage1/dependency/model/Scope.java
new file mode 100644
index 0000000..bf5a34a
--- /dev/null
+++ b/stage1/src/main/java/org/polyfrost/oneconfig/loader/stage1/dependency/model/Scope.java
@@ -0,0 +1,29 @@
+package org.polyfrost.oneconfig.loader.stage1.dependency.model;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * @author xtrm
+ * @since 1.1.0
+ */
+@Getter
+@RequiredArgsConstructor
+public enum Scope {
+ RUNTIME("runtime"),
+ COMPILE("compile"),
+ PROVIDED("provided"),
+ TEST("test"),
+ UNKNOWN("unknown");
+
+ private final String title;
+
+ public static Scope fromTitle(String title) {
+ for (Scope scope : values()) {
+ if (scope.title.equalsIgnoreCase(title)) {
+ return scope;
+ }
+ }
+ return UNKNOWN;
+ }
+}
diff --git a/stage1/src/main/java/org/polyfrost/oneconfig/loader/stage1/util/Lazy.java b/stage1/src/main/java/org/polyfrost/oneconfig/loader/stage1/util/Lazy.java
new file mode 100644
index 0000000..5b754c4
--- /dev/null
+++ b/stage1/src/main/java/org/polyfrost/oneconfig/loader/stage1/util/Lazy.java
@@ -0,0 +1,31 @@
+package org.polyfrost.oneconfig.loader.stage1.util;
+
+import java.util.function.Supplier;
+
+/**
+ * Lazy provider for a value.
+ *
+ * @param the type of the value
+ *
+ * @author xtrm
+ * @since 1.1.0
+ */
+public class Lazy implements Supplier {
+ private final Supplier supplier;
+ private transient T value;
+
+ public Lazy(Supplier supplier) {
+ this.supplier = supplier;
+ }
+
+ public synchronized T get() {
+ if (value == null) {
+ value = supplier.get();
+ }
+ return value;
+ }
+
+ public static Lazy of(Supplier supplier) {
+ return new Lazy<>(supplier);
+ }
+}
diff --git a/stage1/src/main/java/org/polyfrost/oneconfig/loader/stage1/util/SystemProperties.java b/stage1/src/main/java/org/polyfrost/oneconfig/loader/stage1/util/SystemProperties.java
new file mode 100644
index 0000000..b52d552
--- /dev/null
+++ b/stage1/src/main/java/org/polyfrost/oneconfig/loader/stage1/util/SystemProperties.java
@@ -0,0 +1,110 @@
+package org.polyfrost.oneconfig.loader.stage1.util;
+
+import lombok.Getter;
+
+import java.io.File;
+import java.net.URI;
+import java.net.URL;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.function.Supplier;
+
+/**
+ * @author xtrm
+ * @since 1.1.0
+ */
+public class SystemProperties {
+ private static final String PREFIX = "oneconfig.loader.";
+ public static final Property STORAGE_DIRECTORY = property(
+ "storageDirectory",
+ () -> XDG.provideDataDir("OneConfig")
+ .resolve("loader")
+ );
+
+ public static final Property SKIP_MC_LOOKUP =
+ property("libraries.skipMinecraftLookup", false);
+
+ // Remove overrides
+ public static final Property BRANCH =
+ property("branch", () -> "releases");
+ public static final Property REPOSITORY_URL =
+ property("repositoryUri", () -> URI.create("https://repo.polyfrost.org/" + BRANCH.get()));
+
+ // Artifact overrides
+ public static final Property ONECONFIG_GROUP =
+ property("oneconfig.group", "org.polyfrost");
+ public static final Property ONECONFIG_ARTIFACT =
+ property("oneconfig.artifact", null);
+ public static final Property ONECONFIG_VERSION =
+ property("oneconfig.version", null);
+
+ private SystemProperties() {
+ throw new UnsupportedOperationException("This is a utility class and cannot be instantiated");
+ }
+
+ @SuppressWarnings("unchecked")
+ private static Property property(String key, Supplier defaultSupplier, T... reified) {
+ return new Property<>(key, defaultSupplier, (Class) reified.getClass().getComponentType());
+ }
+
+ @SuppressWarnings("unchecked")
+ private static Property property(String key, T defaultValue, T... reified) {
+ return new Property<>(key, defaultValue, (Class) reified.getClass().getComponentType());
+ }
+
+ @Getter
+ public static class Property implements Supplier {
+ private final String key;
+ private final Lazy provider;
+
+ public Property(String key, T defaultValue, Class type) {
+ this(key, () -> defaultValue, type);
+ }
+
+ public Property(String key, Supplier defaultSupplier, Class type) {
+ this.key = PREFIX + key;
+ if (defaultSupplier == null) {
+ defaultSupplier = () -> null;
+ }
+ final Supplier finalDefaultSupplier = defaultSupplier;
+ this.provider = Lazy.of(() -> {
+ String value = System.getProperty(key);
+ return value == null ? finalDefaultSupplier.get() : parseProperty(value, type);
+ });
+ }
+
+ public T get() {
+ return provider.get();
+ }
+
+ private static T parseProperty(String value, Class type) {
+ if (type == String.class) {
+ return type.cast(value);
+ } else if (type == Byte.class || type == Character.class) {
+ return type.cast(value.charAt(0));
+ } else if (type == Short.class) {
+ return type.cast(Short.parseShort(value));
+ } else if (type == Integer.class) {
+ return type.cast(Integer.parseInt(value));
+ } else if (type == Long.class) {
+ return type.cast(Long.parseLong(value));
+ } else if (type == Boolean.class) {
+ return type.cast(Boolean.parseBoolean(value));
+ } else if (type == Float.class) {
+ return type.cast(Float.parseFloat(value));
+ } else if (type == Double.class) {
+ return type.cast(Double.parseDouble(value));
+ } else if (type == URI.class) {
+ return type.cast(URI.create(value));
+ } else if (type == URL.class) {
+ throw new UnsupportedOperationException("URL is not supported, use URI instead");
+ } else if (type == Path.class) {
+ return type.cast(Paths.get(value));
+ } else if (type == File.class) {
+ throw new UnsupportedOperationException("File is not supported, use Path instead");
+ } else {
+ throw new IllegalArgumentException("Unsupported property type: " + type);
+ }
+ }
+ }
+}
diff --git a/stage1/src/main/java/org/polyfrost/oneconfig/loader/stage1/util/XDG.java b/stage1/src/main/java/org/polyfrost/oneconfig/loader/stage1/util/XDG.java
new file mode 100644
index 0000000..8582436
--- /dev/null
+++ b/stage1/src/main/java/org/polyfrost/oneconfig/loader/stage1/util/XDG.java
@@ -0,0 +1,147 @@
+package org.polyfrost.oneconfig.loader.stage1.util;
+
+import lombok.Data;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Utility around the base directories for each operating-system to standardize storage.
+ *
+ * @author xtrm
+ * @since 1.1.0
+ */
+public class XDG {
+ private static final String OS_NAME = System.getProperty("os.name").toLowerCase();
+ private static final List UNIX_ALIASES =
+ Arrays.asList("cygwin", "nix", "nux", "aix", "bsd", "sunos");
+ private static final boolean IS_UNIX = UNIX_ALIASES.stream().anyMatch(OS_NAME::contains);
+ private static final boolean IS_MAC = OS_NAME.contains("mac");
+ private static final boolean IS_WINDOWS = OS_NAME.contains("win");
+
+ private static final Lazy USER_HOME = Lazy.of(XDG::fetchUserHome);
+ private static final Lazy DATA_DIR = Lazy.of(XDG::fetchDataDir);
+ private static final Lazy CACHE_DIR = Lazy.of(XDG::fetchCacheDir);
+
+ private XDG() {
+ throw new UnsupportedOperationException("This is a utility class and cannot be instantiated");
+ }
+
+ public static ApplicationStore provideApplicationStore(String applicationName) throws IOException {
+ return new ApplicationStore(
+ provideCacheDir(applicationName),
+ provideDataDir(applicationName),
+ Files.createTempDirectory(applicationName)
+ ).ensureCreated();
+ }
+
+ public static Path provideDataDir(String applicationName) {
+ return DATA_DIR.get().resolve(applicationName);
+ }
+
+ public static Path provideCacheDir(String applicationName) {
+ return CACHE_DIR.get().resolve(applicationName);
+ }
+
+ private static Path fetchUserHome() {
+ Map env = System.getenv();
+
+ Path userHome = null;
+ if (IS_WINDOWS) {
+ userHome = findFromEnv("USERPROFILE");
+ if (userHome == null) {
+ String homePath = env.get("HOMEPATH");
+ if (homePath != null) {
+ userHome = ensureExists(Paths.get(
+ env.getOrDefault("HOMEDRIVE", "C:") + homePath
+ ));
+ }
+ }
+ }
+
+ //@formatter:off
+ if (userHome == null) { userHome = findFromEnv("HOME"); }
+ if (userHome == null) { userHome = ensureExists(Paths.get(System.getProperty("user.home"))); }
+ if (userHome == null) { throw new IllegalStateException("Could not find user home directory"); }
+ //@formatter:on
+
+ return userHome;
+ }
+
+ private static Path fetchDataDir() {
+ Path userDirectory = USER_HOME.get();
+
+ if (IS_UNIX || IS_MAC) {
+ Path xdgDataHome = findFromEnv("XDG_DATA_HOME");
+ if (xdgDataHome != null) {
+ return xdgDataHome;
+ }
+ }
+
+ Path dataDirectory = null;
+
+ if (IS_WINDOWS) {
+ dataDirectory = findFromEnv("APPDATA");
+ if (dataDirectory == null) {
+ dataDirectory = userDirectory.resolve("AppData/Roaming");
+ }
+ } else if (IS_MAC) {
+ dataDirectory = userDirectory.resolve("Library/Application Support");
+ } else if (IS_UNIX) {
+ dataDirectory = userDirectory.resolve(".local/share");
+ }
+
+ if (dataDirectory == null) {
+ dataDirectory = userDirectory;
+ }
+
+ return dataDirectory;
+ }
+
+ private static Path fetchCacheDir() {
+ if (IS_UNIX || IS_MAC) {
+ Path xdgCacheHome = findFromEnv("XDG_CACHE_HOME");
+ if (xdgCacheHome != null) {
+ return xdgCacheHome;
+ }
+ }
+ if (IS_UNIX) {
+ return USER_HOME.get().resolve(".cache");
+ }
+
+ return DATA_DIR.get();
+ }
+
+ private static @Nullable Path findFromEnv(String env) {
+ String path = System.getenv(env);
+ if (path != null && !path.isEmpty()) {
+ return ensureExists(Paths.get(path));
+ }
+ return null;
+ }
+
+ private static @Nullable Path ensureExists(Path potential) {
+ if (Files.exists(potential)) {
+ return potential;
+ }
+ return null;
+ }
+
+ public static @Data class ApplicationStore {
+ private final Path cacheDir;
+ private final Path dataDir;
+ private final Path tmpDir;
+
+ private ApplicationStore ensureCreated() throws IOException {
+ Files.createDirectories(cacheDir);
+ Files.createDirectories(dataDir);
+ return this;
+ }
+ }
+}