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; + } + } +}