getExternalFilesFromJson(String jsonUrl)
{
try
{
return getExternalFilesFromJson(new URL(jsonUrl));
} catch (Exception e)
{
throw new FlowUpdaterException(e);
}
}
/**
* Get the path of the external file.
* @return the path of the external file.
*/
public String getPath()
{
return this.path;
}
/**
* Get the url of the external file.
* @return the url of the external file.
*/
public String getDownloadURL()
{
return this.downloadURL;
}
/**
* Get the sha1 of the external file.
* @return the sha1 of the external file.
*/
public String getSha1()
{
return this.sha1;
}
/**
* Get the size of the external file.
* @return the size of the external file.
*/
public long getSize()
{
return this.size;
}
/**
* Should {@link fr.flowarg.flowupdater.utils.ExternalFileDeleter} check the file?
* @return if the external file deleter should check and delete the file.
*/
public boolean isUpdate()
{
return this.update;
}
}
================================================
FILE: src/main/java/fr/flowarg/flowupdater/download/json/MCP.java
================================================
package fr.flowarg.flowupdater.download.json;
import com.google.gson.JsonObject;
import fr.flowarg.flowupdater.utils.FlowUpdaterException;
import fr.flowarg.flowupdater.utils.IOUtils;
import org.jetbrains.annotations.NotNull;
import java.net.MalformedURLException;
import java.net.URL;
/**
* This class represents an MCP object.
*/
public class MCP
{
private final String clientURL;
private final String clientSha1;
private final long clientSize;
/**
* Construct a new MCP object.
* @param clientURL URL of client.jar
* @param clientSha1 SHA1 of client.jar
* @param clientSize Size (bytes) of client.jar
*/
public MCP(String clientURL, String clientSha1, long clientSize)
{
this.clientURL = clientURL;
this.clientSha1 = clientSha1;
this.clientSize = clientSize;
}
/**
* Provide an MCP instance from a JSON file.
* Template of a JSON file :
*
* {
* "clientURL": "https://url.com/launcher/client.jar",
* "clientSha1": "9b0a9d70320811e7af2e8741653f029151a6719a",
* "clientSize": 1234
* }
*
* @param jsonUrl the JSON file URL.
* @return the MCP instance.
*/
public static @NotNull MCP getMCPFromJson(URL jsonUrl)
{
final JsonObject object = IOUtils.readJson(jsonUrl).getAsJsonObject();
return new MCP(object.get("clientURL").getAsString(), object.get("clientSha1").getAsString(), object.get("clientSize").getAsLong());
}
/**
* Provide an MCP instance from a JSON file.
* @param jsonUrl the JSON file URL.
* @return the MCP instance.
*/
public static @NotNull MCP getMCPFromJson(String jsonUrl)
{
try
{
return getMCPFromJson(new URL(jsonUrl));
} catch (Exception e)
{
throw new FlowUpdaterException(e);
}
}
/**
* Return the client url.
* @return the client url.
*/
public String getClientURL()
{
return this.clientURL;
}
/**
* Return the client sha1.
* @return the client sha1.
*/
public String getClientSha1()
{
return this.clientSha1;
}
/**
* Return the client size.
* @return the client size.
*/
public long getClientSize()
{
return this.clientSize;
}
}
================================================
FILE: src/main/java/fr/flowarg/flowupdater/download/json/Mod.java
================================================
package fr.flowarg.flowupdater.download.json;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import fr.flowarg.flowupdater.utils.FlowUpdaterException;
import fr.flowarg.flowupdater.utils.IOUtils;
import org.jetbrains.annotations.NotNull;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
/**
* This class represents a Mod object.
*/
public class Mod
{
private final String name;
private final String sha1;
private final long size;
private final String downloadURL;
/**
* Construct a new Mod object.
* @param name Name of mod file.
* @param downloadURL Mod download URL.
* @param sha1 Sha1 of mod file.
* @param size Size of mod file.
*/
public Mod(String name, String downloadURL, String sha1, long size)
{
this.name = name;
this.downloadURL = downloadURL;
this.sha1 = sha1;
this.size = size;
}
/**
* Provide a List of Mods from a JSON file.
* Template of a JSON file :
*
* {
* "mods": [
* {
* "name": "KeyStroke",
* "downloadURL": "https://url.com/launcher/mods/KeyStroke.jar",
* "sha1": "70e564892989d8bbc6f45c895df56c5db9378f48",
* "size": 1234
* },
* {
* "name": "JourneyMap",
* "downloadURL": "https://url.com/launcher/mods/JourneyMap.jar",
* "sha1": "eef74b3fbab6400cb14b02439cf092cca3c2125c",
* "size": 1234
* }
* ]
* }
*
* @param jsonUrl the JSON file URL.
* @return a Mod list.
*/
public static @NotNull List getModsFromJson(URL jsonUrl)
{
final List result = new ArrayList<>();
final JsonObject object = IOUtils.readJson(jsonUrl).getAsJsonObject();
final JsonArray mods = object.getAsJsonArray("mods");
mods.forEach(modElement -> result.add(fromJson(modElement)));
return result;
}
public static Mod fromJson(JsonElement modElement)
{
final JsonObject obj = modElement.getAsJsonObject();
return new Mod(
obj.get("name").getAsString(),
obj.get("downloadURL").getAsString(),
obj.get("sha1").getAsString(),
obj.get("size").getAsLong()
);
}
/**
* Provide a List of Mods from a JSON file.
* Template of a JSON file :
* @param jsonUrl the JSON file URL.
* @return a Mod list.
*/
public static @NotNull List getModsFromJson(String jsonUrl)
{
try
{
return getModsFromJson(new URL(jsonUrl));
}
catch (Exception e)
{
throw new FlowUpdaterException(e);
}
}
/**
* Get the mod name.
* @return the mod name.
*/
public String getName()
{
return this.name;
}
/**
* Get the sha1 of the mod.
* @return the sha1 of the mod.
*/
public String getSha1()
{
return this.sha1;
}
/**
* Get the mod size.
* @return the mod size.
*/
public long getSize()
{
return this.size;
}
/**
* Get the mod url.
* @return the mod url.
*/
public String getDownloadURL()
{
return this.downloadURL;
}
}
================================================
FILE: src/main/java/fr/flowarg/flowupdater/download/json/ModrinthModPackInfo.java
================================================
package fr.flowarg.flowupdater.download.json;
public class ModrinthModPackInfo extends ModrinthVersionInfo
{
private final boolean installExtFiles;
private final String[] excluded;
public ModrinthModPackInfo(String projectReference, String versionNumber, boolean installExtFiles, String... excluded)
{
super(projectReference, versionNumber);
this.installExtFiles = installExtFiles;
this.excluded = excluded;
}
public ModrinthModPackInfo(String versionId, boolean installExtFiles, String... excluded)
{
super(versionId);
this.installExtFiles = installExtFiles;
this.excluded = excluded;
}
/**
* Get the {@link #installExtFiles} option.
* @return the {@link #installExtFiles} option.
*/
public boolean isInstallExtFiles()
{
return this.installExtFiles;
}
/**
* Get the excluded mods.
* @return the excluded mods.
*/
public String[] getExcluded()
{
return this.excluded;
}
}
================================================
FILE: src/main/java/fr/flowarg/flowupdater/download/json/ModrinthVersionInfo.java
================================================
package fr.flowarg.flowupdater.download.json;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonNull;
import com.google.gson.JsonObject;
import fr.flowarg.flowupdater.utils.FlowUpdaterException;
import fr.flowarg.flowupdater.utils.IOUtils;
import org.jetbrains.annotations.NotNull;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
public class ModrinthVersionInfo
{
private String projectReference = "";
private String versionNumber = "";
private String versionId = "";
/**
* Construct a new ModrinthVersionInfo object.
* @param projectReference the project reference can be slug or id.
* @param versionNumber the version number (and NOT the version name unless they are the same).
*/
public ModrinthVersionInfo(String projectReference, String versionNumber)
{
this.projectReference = projectReference.trim();
this.versionNumber = versionNumber.trim();
}
/**
* Construct a new ModrinthVersionInfo object.
* This constructor doesn't need a project reference because
* we can access the version without any project information.
* @param versionId the version id.
*/
public ModrinthVersionInfo(String versionId)
{
this.versionId = versionId.trim();
}
public static @NotNull List getModrinthVersionsFromJson(URL jsonUrl)
{
final List result = new ArrayList<>();
final JsonObject object = IOUtils.readJson(jsonUrl).getAsJsonObject();
final JsonArray mods = object.getAsJsonArray("modrinthMods");
mods.forEach(modElement -> {
final JsonObject obj = modElement.getAsJsonObject();
final JsonElement versionIdElement = obj.get("versionId");
if(versionIdElement instanceof JsonNull)
result.add(new ModrinthVersionInfo(obj.get("projectReference").getAsString(), obj.get("versionNumber").getAsString()));
else result.add(new ModrinthVersionInfo(versionIdElement.getAsString()));
});
return result;
}
public static @NotNull List getModrinthVersionsFromJson(String jsonUrl)
{
try
{
return getModrinthVersionsFromJson(new URL(jsonUrl));
} catch (Exception e)
{
throw new FlowUpdaterException(e);
}
}
public String getProjectReference()
{
return this.projectReference;
}
public String getVersionNumber()
{
return this.versionNumber;
}
public String getVersionId()
{
return this.versionId;
}
}
================================================
FILE: src/main/java/fr/flowarg/flowupdater/download/json/OptiFineInfo.java
================================================
package fr.flowarg.flowupdater.download.json;
/**
* This class represents an OptiFineInfo object.
*/
public class OptiFineInfo
{
private final String version;
private final boolean preview;
/**
* Construct a new OptiFineInfo object.
* @param version the OptiFine's version.
* @param preview if the version is a preview.
*/
public OptiFineInfo(String version, boolean preview)
{
this.version = version;
this.preview = preview;
}
/**
* Construct a new OptiFineInfo object, use {@link OptiFineInfo#OptiFineInfo(String, boolean)} .
* @param version the OptiFine's version.
*/
public OptiFineInfo(String version)
{
this(version, version.startsWith("preview_"));
}
/**
* Get the OptiFine's version.
* @return the OptiFine's version.
*/
public String getVersion()
{
return this.version;
}
/**
* Is the version a preview?
* @return if the version is a preview or not.
*/
public boolean isPreview()
{
return this.preview;
}
}
================================================
FILE: src/main/java/fr/flowarg/flowupdater/download/json/package-info.java
================================================
/**
* This package contains some objects that can be/are parsed as a JSON.
*/
package fr.flowarg.flowupdater.download.json;
================================================
FILE: src/main/java/fr/flowarg/flowupdater/download/package-info.java
================================================
/**
* This package contains some things about download stuff.
*/
package fr.flowarg.flowupdater.download;
================================================
FILE: src/main/java/fr/flowarg/flowupdater/integrations/Integration.java
================================================
package fr.flowarg.flowupdater.integrations;
import fr.flowarg.flowlogger.ILogger;
import java.nio.file.Files;
import java.nio.file.Path;
/**
* The new Integration system replaces an old plugin system
* which had some problems such as unavailability to communicate directly with FlowUpdater.
* This new system is easier to use: no more annoying updater's options, no more extra-dependencies.
* Polymorphism and inheritance can now be used to avoid code duplication.
*/
public abstract class Integration
{
protected final ILogger logger;
protected final Path folder;
/**
* Default constructor of a basic Integration.
* @param logger the logger used.
* @param folder the folder where the plugin can work.
* @throws Exception if an error occurred.
*/
public Integration(ILogger logger, Path folder) throws Exception
{
this.logger = logger;
this.folder = folder;
Files.createDirectories(this.folder);
}
}
================================================
FILE: src/main/java/fr/flowarg/flowupdater/integrations/IntegrationManager.java
================================================
package fr.flowarg.flowupdater.integrations;
import fr.flowarg.flowio.FileUtils;
import fr.flowarg.flowlogger.ILogger;
import fr.flowarg.flowupdater.FlowUpdater;
import fr.flowarg.flowupdater.download.DownloadList;
import fr.flowarg.flowupdater.download.IProgressCallback;
import fr.flowarg.flowupdater.download.Step;
import fr.flowarg.flowupdater.download.json.*;
import fr.flowarg.flowupdater.integrations.curseforgeintegration.CurseForgeIntegration;
import fr.flowarg.flowupdater.integrations.curseforgeintegration.CurseModPack;
import fr.flowarg.flowupdater.integrations.curseforgeintegration.ICurseForgeCompatible;
import fr.flowarg.flowupdater.integrations.modrinthintegration.IModrinthCompatible;
import fr.flowarg.flowupdater.integrations.modrinthintegration.ModrinthIntegration;
import fr.flowarg.flowupdater.integrations.modrinthintegration.ModrinthModPack;
import fr.flowarg.flowupdater.integrations.optifineintegration.IOptiFineCompatible;
import fr.flowarg.flowupdater.integrations.optifineintegration.OptiFine;
import fr.flowarg.flowupdater.integrations.optifineintegration.OptiFineIntegration;
import fr.flowarg.flowupdater.utils.FlowUpdaterException;
import org.jetbrains.annotations.NotNull;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
/**
* The integration manager loads integration's stuff at the startup of FlowUpdater.
*/
public class IntegrationManager
{
private final IProgressCallback progressCallback;
private final ILogger logger;
private final DownloadList downloadList;
/**
* Construct a new Integration Manager.
* @param updater a {@link FlowUpdater} instance.
*/
public IntegrationManager(@NotNull FlowUpdater updater)
{
this.progressCallback = updater.getCallback();
this.logger = updater.getLogger();
this.downloadList = updater.getDownloadList();
}
/**
* This method loads the CurseForge integration and fetches some data.
* @param dir the installation directory.
* @param curseForgeCompatible a version that accepts CurseForge's feature stuff.
*/
public void loadCurseForgeIntegration(Path dir, ICurseForgeCompatible curseForgeCompatible)
{
this.progressCallback.step(Step.INTEGRATION);
try
{
final CurseModPackInfo modPackInfo = curseForgeCompatible.getCurseModPackInfo();
final List allCurseMods = new ArrayList<>();
if(curseForgeCompatible.getCurseMods().isEmpty() && modPackInfo == null)
{
curseForgeCompatible.setAllCurseMods(allCurseMods);
return;
}
final CurseForgeIntegration curseForgeIntegration = new CurseForgeIntegration(this.logger, dir.getParent().resolve(".cfp"));
for (CurseFileInfo info : curseForgeCompatible.getCurseMods())
{
try {
final Mod mod = curseForgeIntegration.fetchMod(info);
if(mod == null)
break;
this.checkMod(mod, allCurseMods, dir);
}
catch (Exception e)
{
this.logger.printStackTrace(e);
}
}
if (modPackInfo == null)
{
curseForgeCompatible.setAllCurseMods(allCurseMods);
return;
}
this.progressCallback.step(Step.MOD_PACK);
final CurseModPack modPack = curseForgeIntegration.getCurseModPack(modPackInfo);
this.logger.info(String.format("Loading mod pack: %s (%s) by %s.", modPack.getName(), modPack.getVersion(), modPack.getAuthor()));
modPack.getMods().forEach(mod -> {
allCurseMods.add(mod);
try
{
final Path filePath = dir.resolve(mod.getName());
boolean flag = false;
for (String exclude : modPackInfo.getExcluded())
{
if (!mod.getName().equalsIgnoreCase(exclude)) continue;
flag = !mod.isRequired();
break;
}
if(flag) return;
if(Files.exists(filePath)
&& Files.size(filePath) == mod.getSize()
&& (mod.getSha1().isEmpty() || FileUtils.getSHA1(filePath).equalsIgnoreCase(mod.getSha1())))
return;
Files.deleteIfExists(filePath);
this.downloadList.getMods().add(mod);
} catch (Exception e)
{
this.logger.printStackTrace(e);
}
});
curseForgeCompatible.setAllCurseMods(allCurseMods);
}
catch (Exception e)
{
throw new FlowUpdaterException(e);
}
}
public void loadModrinthIntegration(Path dir, IModrinthCompatible modrinthCompatible)
{
try
{
final ModrinthModPackInfo modPackInfo = modrinthCompatible.getModrinthModPackInfo();
final List allModrinthMods = new ArrayList<>();
if(modrinthCompatible.getModrinthMods().isEmpty() && modPackInfo == null)
{
modrinthCompatible.setAllModrinthMods(allModrinthMods);
return;
}
final ModrinthIntegration modrinthIntegration = new ModrinthIntegration(this.logger, dir.getParent().resolve(".modrinth"));
for (ModrinthVersionInfo info : modrinthCompatible.getModrinthMods())
{
final Mod mod = modrinthIntegration.fetchMod(info);
this.checkMod(mod, allModrinthMods, dir);
}
if (modPackInfo == null)
{
modrinthCompatible.setAllModrinthMods(allModrinthMods);
return;
}
this.progressCallback.step(Step.MOD_PACK);
final ModrinthModPack modPack = modrinthIntegration.getCurseModPack(modPackInfo);
this.logger.info(String.format("Loading mod pack: %s (%s).", modPack.getName(), modPack.getVersion()));
modrinthCompatible.setModrinthModPack(modPack);
for (Mod mod : modPack.getMods())
this.checkMod(mod, allModrinthMods, dir);
modrinthCompatible.setAllModrinthMods(allModrinthMods);
}
catch (Exception e)
{
throw new FlowUpdaterException(e);
}
}
/**
* This method loads the OptiFine integration and fetches OptiFine data.
* @param dir the installation directory.
* @param optiFineCompatible the current Forge version.
*/
public void loadOptiFineIntegration(Path dir, @NotNull IOptiFineCompatible optiFineCompatible)
{
final OptiFineInfo info = optiFineCompatible.getOptiFineInfo();
if(info == null)
return;
try
{
final OptiFineIntegration optifineIntegration = new OptiFineIntegration(this.logger, dir.getParent().resolve(".op"));
final OptiFine optifine = optifineIntegration.getOptiFine(info.getVersion(), info.isPreview());
this.downloadList.setOptiFine(optifine);
} catch (Exception e)
{
throw new FlowUpdaterException(e);
}
}
private void checkMod(Mod mod, @NotNull List allMods, @NotNull Path dir) throws Exception
{
allMods.add(mod);
final Path filePath = dir.resolve(mod.getName());
if(Files.exists(filePath)
&& Files.size(filePath) == mod.getSize()
&& (mod.getSha1().isEmpty() || FileUtils.getSHA1(filePath).equalsIgnoreCase(mod.getSha1())))
return;
Files.deleteIfExists(filePath);
this.downloadList.getMods().add(mod);
}
}
================================================
FILE: src/main/java/fr/flowarg/flowupdater/integrations/curseforgeintegration/CurseForgeIntegration.java
================================================
package fr.flowarg.flowupdater.integrations.curseforgeintegration;
import com.google.gson.*;
import fr.flowarg.flowio.FileUtils;
import fr.flowarg.flowlogger.ILogger;
import fr.flowarg.flowstringer.StringUtils;
import fr.flowarg.flowupdater.download.json.CurseFileInfo;
import fr.flowarg.flowupdater.download.json.CurseModPackInfo;
import fr.flowarg.flowupdater.download.json.Mod;
import fr.flowarg.flowupdater.integrations.Integration;
import fr.flowarg.flowupdater.utils.IOUtils;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.*;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicReference;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
/**
* This integration supports all CurseForge stuff that FlowUpdater needs such as retrieve mods and mod packs from CurseForge.
*/
public class CurseForgeIntegration extends Integration
{
private static final String CF_API_URL = "https://api.curseforge.com";
private static final String CF_API_KEY = "JDJhJDEwJHBFZjhacXFwWE4zbVdtLm5aZ2pBMC5kdm9ibnhlV3hQZWZma2Q5ZEhCRWFid2VaUWh2cUtpJDJhJ";
private static final String MOD_FILE_ENDPOINT = "/v1/mods/{modId}/files/{fileId}";
private boolean manifestChanged = false;
/**
* Default constructor of a basic Integration.
*
* @param logger the logger used.
* @param folder the folder where the plugin can work.
* @throws Exception if an error occurred.
*/
public CurseForgeIntegration(ILogger logger, Path folder) throws Exception
{
super(logger, folder);
}
public Mod fetchMod(CurseFileInfo curseFileInfo) throws CurseForgeException
{
try
{
return this.parseModFile(this.fetchModLink(curseFileInfo));
} catch (Exception e)
{
throw new CurseForgeException(String.format("Failed to fetch mod project id: %d file id: %d", curseFileInfo.getProjectID(), curseFileInfo.getFileID()), e);
}
}
public static class CurseForgeException extends Exception
{
public CurseForgeException(String message, Throwable cause)
{
super(message, cause);
}
}
public String fetchModLink(@NotNull CurseFileInfo curseFileInfo)
{
final String url = CF_API_URL + MOD_FILE_ENDPOINT
.replace("{modId}", String.valueOf(curseFileInfo.getProjectID()))
.replace("{fileId}", String.valueOf(curseFileInfo.getFileID()));
return this.makeRequest(url);
}
/**
* Make a request to the CurseForge API.
* Oh my god, fuck Java 8 HTTP API, it's so fucking bad. Hope we drop Java 8 as soon as possible.
*
* @param url the url to request.
* @return the response of the request.
*/
private @NotNull String makeRequest(String url)
{
HttpURLConnection connection = null;
try
{
connection = (HttpURLConnection)new URL(url).openConnection();
connection.setRequestMethod("GET");
connection.setInstanceFollowRedirects(true);
connection.setUseCaches(false);
connection.setRequestProperty("Accept", "application/json");
connection.setRequestProperty("x-api-key", this.getCurseForgeAPIKey());
return IOUtils.getContent(connection.getInputStream());
}
catch (Exception e)
{
return "";
}
finally
{
if(connection != null)
connection.disconnect();
}
}
/**
* Parse the CurseForge API to retrieve the mod file.
*/
private @NotNull Mod parseModFile(String jsonResponse)
{
final JsonObject data = JsonParser.parseString(jsonResponse).getAsJsonObject().getAsJsonObject("data");
final String fileName = data.get("fileName").getAsString();
final JsonElement downloadURLElement = data.get("downloadUrl");
String downloadURL;
if(downloadURLElement instanceof JsonNull)
{
logger.warn(String.format("Mod file %s not available. The download can fail because of this! %s", data.get("displayName").getAsString(), jsonResponse));
final String id = Integer.toString(data.get("id").getAsInt());
downloadURL = String.format("https://edge.forgecdn.net/files/%s/%s/%s", id.substring(0, 4), id.substring(4), fileName);
}
else downloadURL = downloadURLElement.getAsString();
final long fileLength = data.get("fileLength").getAsLong();
final AtomicReference sha1 = new AtomicReference<>("");
data.getAsJsonArray("hashes").forEach(hashObject -> {
final String hash = hashObject.getAsJsonObject().get("value").getAsString();
if(hash.length() == 40)
sha1.set(hash);
});
return new Mod(fileName, downloadURL, sha1.get(), fileLength);
}
/**
* Get a CurseForge's mod pack object with a project identifier and a file identifier.
* @param info CurseForge's mod pack info.
* @return the curse's mod pack corresponding to given parameters.
* @throws Exception if an error occurred.
*/
public CurseModPack getCurseModPack(CurseModPackInfo info) throws Exception
{
final Path modPackFile = this.checkForUpdate(info);
this.extractModPack(modPackFile, info.isInstallExtFiles());
return this.parseMods();
}
private @NotNull Path checkForUpdate(@NotNull CurseModPackInfo info) throws Exception
{
final String responseData = info.getUrl().isEmpty() ? this.fetchModLink(info) : this.makeRequest(info.getUrl());
final Mod modPackFile = this.parseModFile(responseData);
final Path outPath = this.folder.resolve(modPackFile.getName());
if(Files.notExists(outPath) || (!modPackFile.getSha1().isEmpty() && !FileUtils.getSHA1(outPath).equalsIgnoreCase(modPackFile.getSha1())) || Files.size(outPath) != modPackFile.getSize())
IOUtils.download(this.logger, new URL(modPackFile.getDownloadURL()), outPath);
return outPath;
}
private void extractModPack(@NotNull Path out, boolean installExtFiles) throws Exception
{
this.logger.info("Extracting mod pack...");
final ZipFile zipFile = new ZipFile(out.toFile(), ZipFile.OPEN_READ, StandardCharsets.UTF_8);
final Path dirPath = this.folder.getParent();
final Enumeration extends ZipEntry> entries = zipFile.entries();
while (entries.hasMoreElements())
{
final ZipEntry entry = entries.nextElement();
final Path flPath = dirPath.resolve(StringUtils.empty(entry.getName(), "overrides/"));
final String entryName = entry.getName();
if(entryName.equalsIgnoreCase("manifest.json"))
{
if(Files.notExists(flPath) || entry.getCrc() != FileUtils.getCRC32(flPath))
{
this.manifestChanged = true;
this.transferAndClose(flPath, zipFile, entry);
}
if(!installExtFiles)
break;
continue;
}
if(!installExtFiles)
continue;
if(entryName.equals("modlist.html"))
continue;
if(Files.exists(flPath))
{
if(!Files.isDirectory(flPath))
{
if(entry.getCrc() == FileUtils.getCRC32(flPath))
continue;
}
}
else if (flPath.getFileName().toString().endsWith(flPath.getFileSystem().getSeparator()))
Files.createDirectories(flPath);
if (entry.isDirectory()) continue;
this.transferAndClose(flPath, zipFile, entry);
}
zipFile.close();
}
private @NotNull CurseModPack parseMods() throws Exception
{
this.logger.info("Fetching mods...");
final Path dirPath = this.folder.getParent();
final String manifestJson = StringUtils.toString(Files.readAllLines(dirPath.resolve("manifest.json")));
final JsonObject manifestObj = JsonParser.parseString(manifestJson).getAsJsonObject();
final String modPackName = manifestObj.get("name").getAsString();
final String modPackVersion = manifestObj.get("version").getAsString();
final String modPackAuthor = manifestObj.get("author").getAsString();
final List mods = this.processCacheFile(dirPath, this.populateManifest(manifestObj));
return new CurseModPack(modPackName, modPackVersion, modPackAuthor, mods);
}
private @NotNull List populateManifest(@NotNull JsonObject manifestObj)
{
final List manifestFiles = new ArrayList<>();
manifestObj.getAsJsonArray("files")
.forEach(jsonElement -> manifestFiles.add(ProjectMod.fromJson(jsonElement.getAsJsonObject())));
return manifestFiles;
}
private @NotNull List processCacheFile(@NotNull Path dirPath, List manifestFiles) throws Exception
{
final Path cachePath = dirPath.resolve("manifest.cache.json");
if(Files.notExists(cachePath))
{
Files.createFile(cachePath);
Files.write(cachePath, Collections.singletonList("[]"), StandardCharsets.UTF_8);
}
String json = StringUtils.toString(Files.readAllLines(cachePath, StandardCharsets.UTF_8));
if(this.manifestChanged || json.contains("\"md5\"") || json.contains("\"length\""))
{
Files.delete(cachePath);
Files.createFile(cachePath);
Files.write(cachePath, Collections.singletonList("[]"), StandardCharsets.UTF_8);
json = StringUtils.toString(Files.readAllLines(cachePath, StandardCharsets.UTF_8));
}
return this.deserializeWriteCache(json, manifestFiles, cachePath);
}
@Contract("_, _, _ -> new")
private @NotNull List deserializeWriteCache(String json,
List manifestFiles, Path cachePath) throws Exception
{
final JsonArray cacheArray = JsonParser.parseString(json).getAsJsonArray();
final Queue mods = new ConcurrentLinkedQueue<>();
cacheArray.forEach(jsonElement -> {
final JsonObject object = jsonElement.getAsJsonObject();
final Mod mod = Mod.fromJson(jsonElement);
final ProjectMod projectMod = ProjectMod.fromJson(object);
mods.add(new CurseModPack.CurseModPackMod(mod, projectMod.isRequired()));
manifestFiles.remove(projectMod);
});
IOUtils.executeAsyncForEach(manifestFiles, Executors.newWorkStealingPool(), projectMod -> this.fetchAndSerializeProjectMod(projectMod, cacheArray, mods));
Files.write(cachePath, Collections.singletonList(cacheArray.toString()), StandardCharsets.UTF_8);
return new ArrayList<>(mods);
}
private void fetchAndSerializeProjectMod(@NotNull ProjectMod projectMod, JsonArray cacheArray,
Queue mods)
{
final boolean required = projectMod.isRequired();
try
{
final Mod retrievedMod = this.fetchMod(projectMod);
if(retrievedMod == null)
return;
final CurseModPack.CurseModPackMod mod = new CurseModPack.CurseModPackMod(retrievedMod, required);
final JsonObject inCache = new JsonObject();
inCache.addProperty("name", mod.getName());
inCache.addProperty("downloadURL", mod.getDownloadURL());
inCache.addProperty("sha1", mod.getSha1());
inCache.addProperty("size", mod.getSize());
inCache.addProperty("required", required);
inCache.addProperty("projectID", projectMod.getProjectID());
inCache.addProperty("fileID", projectMod.getFileID());
cacheArray.add(inCache);
mods.add(mod);
}
catch (Exception e)
{
this.logger.printStackTrace(e);
}
}
private void transferAndClose(@NotNull Path flPath, ZipFile zipFile, ZipEntry entry) throws Exception
{
if(Files.notExists(flPath.getParent()))
Files.createDirectories(flPath.getParent());
Files.copy(zipFile.getInputStream(entry), flPath, StandardCopyOption.REPLACE_EXISTING);
}
private static class ProjectMod extends CurseFileInfo
{
private final boolean required;
public ProjectMod(int projectID, int fileID, boolean required)
{
super(projectID, fileID);
this.required = required;
}
private static @NotNull ProjectMod fromJson(@NotNull JsonObject object)
{
return new ProjectMod(object.get("projectID").getAsInt(),
object.get("fileID").getAsInt(),
object.get("required").getAsBoolean());
}
public boolean isRequired()
{
return this.required;
}
}
/**
* Get the CurseForge API Key.
*/
private static String cacheKey = "";
private String getCurseForgeAPIKey()
{
return cacheKey.isEmpty() ? cacheKey = StringUtils.toString(Base64.getDecoder().decode(CF_API_KEY.substring(0, CF_API_KEY.length() - 5))) : cacheKey;
}
}
================================================
FILE: src/main/java/fr/flowarg/flowupdater/integrations/curseforgeintegration/CurseModPack.java
================================================
package fr.flowarg.flowupdater.integrations.curseforgeintegration;
import fr.flowarg.flowupdater.download.json.Mod;
import org.jetbrains.annotations.NotNull;
import java.util.List;
/**
* Basic object that represents a CurseForge's mod pack.
*/
public class CurseModPack
{
private final String name;
private final String version;
private final String author;
private final List mods;
CurseModPack(String name, String version, String author, List mods)
{
this.name = name;
this.version = version;
this.author = author;
this.mods = mods;
}
/**
* Get the mod pack's name.
* @return the mod pack's name.
*/
public String getName()
{
return this.name;
}
/**
* Get the mod pack's version.
* @return the mod pack's version.
*/
public String getVersion()
{
return this.version;
}
/**
* Get the mod pack's author.
* @return the mod pack's author.
*/
public String getAuthor()
{
return this.author;
}
/**
* Get the mods in the mod pack.
* @return the mods in the mod pack.
*/
public List getMods()
{
return this.mods;
}
/**
* A Curse Forge's mod from a mod pack.
*/
public static class CurseModPackMod extends Mod
{
private final boolean required;
CurseModPackMod(String name, String downloadURL, String sha1, long size, boolean required)
{
super(name, downloadURL, sha1, size);
this.required = required;
}
CurseModPackMod(@NotNull Mod base, boolean required)
{
this(base.getName(), base.getDownloadURL(), base.getSha1(), base.getSize(), required);
}
/**
* Is the mod required.
* @return if the mod is required.
*/
public boolean isRequired()
{
return this.required;
}
}
}
================================================
FILE: src/main/java/fr/flowarg/flowupdater/integrations/curseforgeintegration/ICurseForgeCompatible.java
================================================
package fr.flowarg.flowupdater.integrations.curseforgeintegration;
import fr.flowarg.flowupdater.download.json.CurseFileInfo;
import fr.flowarg.flowupdater.download.json.CurseModPackInfo;
import fr.flowarg.flowupdater.download.json.Mod;
import java.util.List;
/**
* This class represents an object that using CurseForge features.
*/
public interface ICurseForgeCompatible
{
/**
* Get all curse mods to update.
* @return all curse mods.
*/
List getCurseMods();
/**
* Get information about the mod pack to update.
* @return mod pack's information.
*/
CurseModPackInfo getCurseModPackInfo();
/**
* Define all curse mods to update.
* @param curseMods curse mods to define.
*/
void setAllCurseMods(List curseMods);
}
================================================
FILE: src/main/java/fr/flowarg/flowupdater/integrations/curseforgeintegration/package-info.java
================================================
/**
* CurseForge Integration package.
*/
package fr.flowarg.flowupdater.integrations.curseforgeintegration;
================================================
FILE: src/main/java/fr/flowarg/flowupdater/integrations/modrinthintegration/IModrinthCompatible.java
================================================
package fr.flowarg.flowupdater.integrations.modrinthintegration;
import fr.flowarg.flowupdater.download.json.Mod;
import fr.flowarg.flowupdater.download.json.ModrinthModPackInfo;
import fr.flowarg.flowupdater.download.json.ModrinthVersionInfo;
import java.util.List;
public interface IModrinthCompatible
{
/**
* Get all modrinth mods to update.
* @return all modrinth mods.
*/
List getModrinthMods();
/**
* Get information about the mod pack to update.
* @return mod pack's information.
*/
ModrinthModPackInfo getModrinthModPackInfo();
/**
* Get the modrinth mod pack.
* @return the modrinth mod pack.
*/
ModrinthModPack getModrinthModPack();
/**
* Define the modrinth mod pack.
* @param modrinthModPack the modrinth mod pack.
*/
void setModrinthModPack(ModrinthModPack modrinthModPack);
/**
* Define all modrinth mods to update.
* @param modrinthMods modrinth mods to define.
*/
void setAllModrinthMods(List modrinthMods);
}
================================================
FILE: src/main/java/fr/flowarg/flowupdater/integrations/modrinthintegration/ModrinthIntegration.java
================================================
package fr.flowarg.flowupdater.integrations.modrinthintegration;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import fr.flowarg.flowio.FileUtils;
import fr.flowarg.flowlogger.ILogger;
import fr.flowarg.flowstringer.StringUtils;
import fr.flowarg.flowupdater.download.json.Mod;
import fr.flowarg.flowupdater.download.json.ModrinthModPackInfo;
import fr.flowarg.flowupdater.download.json.ModrinthVersionInfo;
import fr.flowarg.flowupdater.integrations.Integration;
import fr.flowarg.flowupdater.utils.FlowUpdaterException;
import fr.flowarg.flowupdater.utils.IOUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
public class ModrinthIntegration extends Integration
{
private static final String MODRINTH_URL = "https://api.modrinth.com/v2/";
private static final String MODRINTH_VERSION_ENDPOINT = "version/{versionId}";
private static final String MODRINTH_PROJECT_VERSION_ENDPOINT = "project/{projectId}/version";
private final List builtInMods = new ArrayList<>();
/**
* Default constructor of a basic Integration.
*
* @param logger the logger used.
* @param folder the folder where the plugin can work.
* @throws Exception if an error occurred.
*/
public ModrinthIntegration(ILogger logger, Path folder) throws Exception
{
super(logger, folder);
}
public Mod fetchMod(@NotNull ModrinthVersionInfo versionInfo) throws Exception
{
if(!versionInfo.getVersionId().isEmpty())
{
final URL url = new URL(MODRINTH_URL + MODRINTH_VERSION_ENDPOINT
.replace("{versionId}", versionInfo.getVersionId()));
return this.parseModFile(JsonParser.parseString(IOUtils.getContent(url)).getAsJsonObject());
}
final URL url = new URL(MODRINTH_URL + MODRINTH_PROJECT_VERSION_ENDPOINT.replace("{projectId}", versionInfo.getProjectReference()));
final JsonArray versions = JsonParser.parseString(IOUtils.getContent(url)).getAsJsonArray();
JsonObject version = null;
for (JsonElement jsonElement : versions)
{
if(!jsonElement.getAsJsonObject().get("version_number").getAsString().equals(versionInfo.getVersionNumber()))
continue;
version = jsonElement.getAsJsonObject();
break;
}
if(version == null)
throw new FlowUpdaterException(
"No version found for " + versionInfo.getVersionNumber() +
" in project " + versionInfo.getProjectReference());
return this.parseModFile(version);
}
public Mod parseModFile(@NotNull JsonObject version)
{
final JsonObject fileJson = version.getAsJsonArray("files").get(0).getAsJsonObject();
final String fileName = fileJson.get("filename").getAsString();
final String downloadURL = fileJson.get("url").getAsString();
final String sha1 = fileJson.getAsJsonObject("hashes").get("sha1").getAsString();
final long size = fileJson.get("size").getAsLong();
return new Mod(fileName, downloadURL, sha1, size);
}
/**
* Get a CurseForge's mod pack object with a project identifier and a file identifier.
* @param info CurseForge's mod pack info.
* @return the curse's mod pack corresponding to given parameters.
* @throws Exception if an error occurred.
*/
public ModrinthModPack getCurseModPack(ModrinthModPackInfo info) throws Exception
{
final Path modPackFile = this.checkForUpdate(info);
if(modPackFile == null)
throw new FlowUpdaterException("Can't find the mod pack file with the provided Modrinth mod pack info.");
this.extractModPack(modPackFile, info.isInstallExtFiles());
return this.parseMods();
}
private @Nullable Path checkForUpdate(@NotNull ModrinthModPackInfo info) throws Exception
{
final Mod modPackFile = this.fetchMod(info);
if(modPackFile == null)
{
this.logger.err("This mod pack isn't available anymore on Modrinth (for 3rd parties maybe). ");
return null;
}
final Path outPath = this.folder.resolve(modPackFile.getName());
if(Files.notExists(outPath) || !FileUtils.getSHA1(outPath).equalsIgnoreCase(modPackFile.getSha1()))
IOUtils.download(this.logger, new URL(modPackFile.getDownloadURL()), outPath);
return outPath;
}
private void extractModPack(@NotNull Path out, boolean installExtFiles) throws Exception
{
this.logger.info("Extracting mod pack...");
final ZipFile zipFile = new ZipFile(out.toFile(), ZipFile.OPEN_READ, StandardCharsets.UTF_8);
final Path dirPath = this.folder.getParent();
final Enumeration extends ZipEntry> entries = zipFile.entries();
while (entries.hasMoreElements())
{
final ZipEntry entry = entries.nextElement();
final String entryName = entry.getName();
final Path flPath = dirPath.resolve(StringUtils.empty(StringUtils.empty(entryName, "client-overrides/"), "overrides/"));
if(entryName.equalsIgnoreCase("modrinth.index.json"))
{
if(Files.notExists(flPath) || entry.getCrc() != FileUtils.getCRC32(flPath))
this.transferAndClose(flPath, zipFile, entry);
continue;
}
final String withoutOverrides = StringUtils.empty(StringUtils.empty(entryName, "overrides/"), "client-overrides/");
if(withoutOverrides.startsWith("mods/") || withoutOverrides.startsWith("mods\\"))
{
final String modName = withoutOverrides.substring(withoutOverrides.lastIndexOf('/') + 1);
final Mod mod = new Mod(modName, "", "", entry.getSize());
this.builtInMods.add(mod);
}
if(!installExtFiles || Files.exists(flPath)) continue;
if (flPath.getFileName().toString().endsWith(flPath.getFileSystem().getSeparator()))
Files.createDirectories(flPath);
if (entry.isDirectory()) continue;
this.transferAndClose(flPath, zipFile, entry);
}
zipFile.close();
}
private @NotNull ModrinthModPack parseMods() throws Exception
{
this.logger.info("Fetching mods...");
final Path dirPath = this.folder.getParent();
final String manifestJson = StringUtils.toString(Files.readAllLines(dirPath.resolve("modrinth.index.json")));
final JsonObject manifestObj = JsonParser.parseString(manifestJson).getAsJsonObject();
final String modPackName = manifestObj.get("name").getAsString();
final String modPackVersion = manifestObj.get("versionId").getAsString();
final List mods = this.parseManifest(manifestObj);
return new ModrinthModPack(modPackName, modPackVersion, mods, this.builtInMods);
}
private @NotNull List parseManifest(@NotNull JsonObject manifestObject)
{
final List mods = new ArrayList<>();
final JsonArray files = manifestObject.getAsJsonArray("files");
files.forEach(jsonElement -> {
final JsonObject file = jsonElement.getAsJsonObject();
if(file.getAsJsonObject("env").get("client").getAsString().equals("unsupported"))
return;
final String name = StringUtils.empty(StringUtils.empty(file.get("path").getAsString(), "mods/"), "mods\\");
final String downloadURL = file.getAsJsonArray("downloads").get(0).getAsString();
final String sha1 = file.getAsJsonObject("hashes").get("sha1").getAsString();
final long size = file.get("fileSize").getAsLong();
mods.add(new Mod(name, downloadURL, sha1, size));
});
return mods;
}
private void transferAndClose(@NotNull Path flPath, ZipFile zipFile, ZipEntry entry) throws Exception
{
if(Files.notExists(flPath.getParent()))
Files.createDirectories(flPath.getParent());
Files.copy(zipFile.getInputStream(entry), flPath, StandardCopyOption.REPLACE_EXISTING);
}
}
================================================
FILE: src/main/java/fr/flowarg/flowupdater/integrations/modrinthintegration/ModrinthModPack.java
================================================
package fr.flowarg.flowupdater.integrations.modrinthintegration;
import fr.flowarg.flowupdater.download.json.Mod;
import java.util.ArrayList;
import java.util.List;
public class ModrinthModPack
{
private final String name;
private final String version;
private final List mods;
private final List builtInMods;
ModrinthModPack(String name, String version, List mods)
{
this(name, version, mods, new ArrayList<>());
}
ModrinthModPack(String name, String version, List mods, List builtInMods)
{
this.name = name;
this.version = version;
this.mods = mods;
this.builtInMods = builtInMods;
}
/**
* Get the mod pack's name.
* @return the mod pack's name.
*/
public String getName()
{
return this.name;
}
/**
* Get the mod pack's version.
* @return the mod pack's version.
*/
public String getVersion()
{
return this.version;
}
/**
* Get the mods in the mod pack.
* @return the mods in the mod pack.
*/
public List getMods()
{
return this.mods;
}
/**
* Get the built-in mods in the mod pack.
* Built-in mods are mods directly put in the mods folder in the .mrpack file.
* They are not downloaded from a remote server.
* This is not a very good way to add mods because it disables some mod verification on these mods.
* We recommend mod pack creators to use the built-in mods feature only for mods that are not available remotely.
* @return the built-in mods in the mod pack.
*/
public List getBuiltInMods()
{
return this.builtInMods;
}
}
================================================
FILE: src/main/java/fr/flowarg/flowupdater/integrations/modrinthintegration/package-info.java
================================================
/**
* Modrinth Integration package.
*/
package fr.flowarg.flowupdater.integrations.modrinthintegration;
================================================
FILE: src/main/java/fr/flowarg/flowupdater/integrations/optifineintegration/IOptiFineCompatible.java
================================================
package fr.flowarg.flowupdater.integrations.optifineintegration;
import fr.flowarg.flowupdater.download.json.OptiFineInfo;
public interface IOptiFineCompatible
{
/**
* Get information about OptiFine to update.
* @return OptiFine's information.
*/
OptiFineInfo getOptiFineInfo();
}
================================================
FILE: src/main/java/fr/flowarg/flowupdater/integrations/optifineintegration/OptiFine.java
================================================
package fr.flowarg.flowupdater.integrations.optifineintegration;
/**
* This class represents a basic OptiFine object.
*/
public class OptiFine
{
private final String name;
private final long size;
OptiFine(String name, long size)
{
this.name = name;
this.size = size;
}
/**
* Get the OptiFine filename.
* @return the OptiFine filename.
*/
public String getName() {
return this.name;
}
/**
* Get the OptiFine file size.
* @return the OptiFine file size.
*/
public long getSize() {
return this.size;
}
}
================================================
FILE: src/main/java/fr/flowarg/flowupdater/integrations/optifineintegration/OptiFineIntegration.java
================================================
package fr.flowarg.flowupdater.integrations.optifineintegration;
import fr.flowarg.flowlogger.ILogger;
import fr.flowarg.flowupdater.integrations.Integration;
import fr.flowarg.flowupdater.utils.FlowUpdaterException;
import fr.flowarg.flowupdater.utils.IOUtils;
import org.jetbrains.annotations.NotNull;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Optional;
/**
* This integration supports the download of OptiFine in any version from the official site
* (OptiFine).
*/
public class OptiFineIntegration extends Integration
{
public OptiFineIntegration(ILogger logger, Path folder) throws Exception
{
super(logger, folder);
}
/**
* Get an OptiFine object from the official website.
* @param optiFineVersion the version of OptiFine
* @param preview if the OptiFine version is a preview.
* @return the object that defines the plugin
*/
public OptiFine getOptiFine(String optiFineVersion, boolean preview)
{
try
{
final String fixedVersion = preview ? (optiFineVersion.startsWith("preview_OptiFine_") ?
optiFineVersion : optiFineVersion.startsWith("OptiFine_") ?
"preview_" + optiFineVersion : "preview_OptiFine_" + optiFineVersion) :
optiFineVersion.startsWith("OptiFine_") ? optiFineVersion : "OptiFine_" + optiFineVersion;
final String name = fixedVersion + ".jar";
final String newUrl = this.getNewURL(name, preview, fixedVersion);
return new OptiFine(name, this.checkForUpdatesAndGetSize(name, newUrl));
}
catch (FlowUpdaterException e)
{
throw e;
}
catch (Exception e)
{
throw new FlowUpdaterException(e);
}
}
private @NotNull String getNewURL(String name, boolean preview, String optiFineVersion)
{
return "https://optifine.net/downloadx?f=" +
name +
"&x=" +
(preview ? this.getJsonPreview(optiFineVersion) : this.getJson(optiFineVersion));
}
private long checkForUpdatesAndGetSize(String name, String newUrl) throws Exception
{
final Path outputPath = this.folder.resolve(name);
if(Files.notExists(outputPath))
IOUtils.download(this.logger, new URL(newUrl), outputPath);
return Files.size(outputPath);
}
private @NotNull String getJson(String optiFineVersion)
{
try
{
final String[] respLine = IOUtils.getContent(new URL("https://optifine.net/adloadx?f=OptiFine_" + optiFineVersion))
.split("\n");
final Optional result = Arrays.stream(respLine).filter(s -> s.contains("downloadx?f=OptiFine")).findFirst();
if(result.isPresent())
return result.get()
.replace("' onclick='onDownload()'>OptiFine " + optiFineVersion.replace("_", " ") +
"", "")
.replace("" + optiFineVersion.replace("_", " ") +
"", "")
.replace(" mods, OptiFine optiFine, ModrinthModPack modrinthModPack) throws Exception
{
if(!this.isUseFileDeleter())
return;
final Set badFiles = new HashSet<>();
final List verifiedFiles = new ArrayList<>();
if(this.modsToIgnore != null)
Arrays.stream(this.modsToIgnore).forEach(fileName -> verifiedFiles.add(modsDir.resolve(fileName)));
else if(this.modsToIgnorePattern != null)
{
FileUtils.list(modsDir).stream().filter(path -> !Files.isDirectory(path)).forEach(path -> {
if(this.modsToIgnorePattern.matcher(path.getFileName().toString()).matches())
verifiedFiles.add(path);
});
}
if(modrinthModPack != null)
modrinthModPack.getBuiltInMods().forEach(mod -> verifiedFiles.add(modsDir.resolve(mod.getName())));
for(Path fileInDir : FileUtils.list(modsDir).stream().filter(path -> !Files.isDirectory(path)).collect(Collectors.toList()))
{
if(verifiedFiles.contains(fileInDir))
continue;
if(mods.isEmpty() && optiFine == null)
{
if (!verifiedFiles.contains(fileInDir))
badFiles.add(fileInDir);
}
else
{
if (optiFine != null)
{
if (optiFine.getName().equalsIgnoreCase(fileInDir.getFileName().toString()))
{
if (Files.size(fileInDir) == optiFine.getSize())
{
badFiles.remove(fileInDir);
verifiedFiles.add(fileInDir);
}
else badFiles.add(fileInDir);
}
else
{
if (!verifiedFiles.contains(fileInDir)) badFiles.add(fileInDir);
}
}
for (Mod mod : mods)
{
if (mod.getName().equalsIgnoreCase(fileInDir.getFileName().toString()))
{
if (Files.size(fileInDir) == mod.getSize() && (mod.getSha1().isEmpty() || FileUtils.getSHA1(fileInDir).equalsIgnoreCase(mod.getSha1())))
{
badFiles.remove(fileInDir);
verifiedFiles.add(fileInDir);
}
else badFiles.add(fileInDir);
}
else
{
if (!verifiedFiles.contains(fileInDir))
badFiles.add(fileInDir);
}
}
}
}
badFiles.forEach(path -> {
try
{
Files.deleteIfExists(path);
} catch (Exception e)
{
logger.printStackTrace(e);
}
});
badFiles.clear();
}
public boolean isUseFileDeleter()
{
return this.useFileDeleter;
}
public String[] getModsToIgnore()
{
return this.modsToIgnore;
}
}
================================================
FILE: src/main/java/fr/flowarg/flowupdater/utils/UpdaterOptions.java
================================================
package fr.flowarg.flowupdater.utils;
import fr.flowarg.flowupdater.utils.builderapi.BuilderArgument;
import fr.flowarg.flowupdater.utils.builderapi.BuilderException;
import fr.flowarg.flowupdater.utils.builderapi.IBuilder;
import java.nio.file.Paths;
/**
* Represent some settings for FlowUpdater
*
* @author FlowArg
*/
public class UpdaterOptions
{
public static final UpdaterOptions DEFAULT = new UpdaterOptions(new ExternalFileDeleter(), true, System.getProperty("java.home") != null ? Paths.get(System.getProperty("java.home"))
.resolve("bin")
.resolve("java")
.toAbsolutePath()
.toString() : "java"
, false);
private final ExternalFileDeleter externalFileDeleter;
private final boolean versionChecker;
private final String javaPath;
private final boolean disableExtFilesAsyncDownload;
private UpdaterOptions(ExternalFileDeleter externalFileDeleter, boolean versionChecker, String javaPath, boolean disableExtFilesAsyncDownload)
{
this.externalFileDeleter = externalFileDeleter;
this.versionChecker = versionChecker;
this.javaPath = javaPath;
this.disableExtFilesAsyncDownload = disableExtFilesAsyncDownload;
}
/**
* The external file deleter is used to check if some external files need to be downloaded.
* Default: {@link fr.flowarg.flowupdater.utils.ExternalFileDeleter}
* @return externalFileDeleter value.
*/
public ExternalFileDeleter getExternalFileDeleter()
{
return this.externalFileDeleter;
}
/**
* Should check the version of FlowUpdater.
* @return true or false.
*/
public boolean isVersionChecker()
{
return this.versionChecker;
}
/**
* The path to the java executable to use with Forge and Fabric installers.
* By default, it's taken from System.getProperty("java.home").
* @return the path to the java executable.
*/
public String getJavaPath()
{
return this.javaPath;
}
/**
* If set to true, external files will be downloaded 1 by 1 (as it always been). Asynchronous downloading of
* external files has been introduced in 1.9.4 in order to fasten the downloading step when a project
* needs many external files.
* @return true if asynchronous downloading of external files is disabled. False otherwise.
*/
public boolean shouldDisableExtFilesAsyncDownload()
{
return this.disableExtFilesAsyncDownload;
}
/**
* Builder of {@link UpdaterOptions}
*/
public static class UpdaterOptionsBuilder implements IBuilder
{
private final BuilderArgument externalFileDeleterArgument = new BuilderArgument<>("External FileDeleter", ExternalFileDeleter::new).optional();
private final BuilderArgument versionChecker = new BuilderArgument<>("VersionChecker", () -> true).optional();
private final BuilderArgument javaPath = new BuilderArgument<>("JavaPath", () ->
System.getProperty("java.home") != null ? Paths.get(System.getProperty("java.home"))
.resolve("bin")
.resolve("java")
.toAbsolutePath()
.toString() : "java")
.optional();
private final BuilderArgument disableExtFilesAsyncDownload = new BuilderArgument<>("DisableExtFilesAsyncDownload", () -> false).optional();
/**
* Append an {@link ExternalFileDeleter} object.
* @param externalFileDeleter the file deleter to define.
* @return the builder.
*/
public UpdaterOptionsBuilder withExternalFileDeleter(ExternalFileDeleter externalFileDeleter)
{
this.externalFileDeleterArgument.set(externalFileDeleter);
return this;
}
/**
* Enable or disable the version checker.
* @param versionChecker the value to define.
* @return the builder.
*/
public UpdaterOptionsBuilder withVersionChecker(boolean versionChecker)
{
this.versionChecker.set(versionChecker);
return this;
}
/**
* Set the path to the java executable to use with Forge and Fabric installers.
* (Directly the java executable, not the java home)
* If you wish to set up the java home, you should use the {@link System#setProperty(String, String)} method
* with the "java.home" key.
* By default, it's taken from {@code System.getProperty("java.home")}.
* @param javaPath the path to the java executable.
* @return the builder.
*/
public UpdaterOptionsBuilder withJavaPath(String javaPath)
{
this.javaPath.set(javaPath);
return this;
}
/**
* Disable asynchronous downloading of external files. See {@link UpdaterOptions#shouldDisableExtFilesAsyncDownload()} for more information.
* @param disableExtFilesAsyncDownload true to disable asynchronous downloading of external files. False otherwise.
* @return the builder.
*/
public UpdaterOptionsBuilder withDisableExtFilesAsyncDownload(boolean disableExtFilesAsyncDownload)
{
this.disableExtFilesAsyncDownload.set(disableExtFilesAsyncDownload);
return this;
}
/**
* Build an {@link UpdaterOptions} object.
*/
@Override
public UpdaterOptions build() throws BuilderException
{
return new UpdaterOptions(
this.externalFileDeleterArgument.get(),
this.versionChecker.get(),
this.javaPath.get(),
this.disableExtFilesAsyncDownload.get()
);
}
}
}
================================================
FILE: src/main/java/fr/flowarg/flowupdater/utils/Version.java
================================================
package fr.flowarg.flowupdater.utils;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public class Version implements Comparable
{
private final List version;
public Version(List version)
{
this.version = version;
}
@Contract("_ -> new")
public static @NotNull Version gen(@NotNull String version)
{
if(version.isEmpty())
throw new IllegalArgumentException("Version cannot be empty.");
final String[] parts = version.split("\\.");
final List versionList = new ArrayList<>();
for (String part : parts)
versionList.add(Integer.parseInt(part));
return new Version(versionList);
}
@Override
public int compareTo(@NotNull Version o)
{
final int thisSize = this.version.size();
final int oSize = o.version.size();
for (int i = 0; i < Math.min(thisSize, oSize); i++)
if (!Objects.equals(this.version.get(i), o.version.get(i)))
return Integer.compare(this.version.get(i), o.version.get(i));
return Integer.compare(thisSize, oSize);
}
@Override
public String toString()
{
final StringBuilder builder = new StringBuilder();
for (int i = 0; i < this.version.size(); i++)
{
builder.append(this.version.get(i));
if (i < this.version.size() - 1)
builder.append(".");
}
return builder.toString();
}
public boolean isNewerThan(@NotNull Version o)
{
return this.compareTo(o) > 0;
}
public boolean isNewerOrEqualTo(@NotNull Version o)
{
return this.compareTo(o) >= 0;
}
public boolean isOlderThan(@NotNull Version o)
{
return this.compareTo(o) < 0;
}
public boolean isOlderOrEqualTo(@NotNull Version o)
{
return this.compareTo(o) <= 0;
}
public boolean isEqualTo(@NotNull Version o)
{
return this.compareTo(o) == 0;
}
public boolean isBetweenOrEqual(@NotNull Version min, @NotNull Version max)
{
return this.isNewerOrEqualTo(min) && this.isOlderOrEqualTo(max);
}
}
================================================
FILE: src/main/java/fr/flowarg/flowupdater/utils/VersionChecker.java
================================================
package fr.flowarg.flowupdater.utils;
import fr.flowarg.flowlogger.ILogger;
import fr.flowarg.flowupdater.FlowUpdater;
public class VersionChecker
{
public static void run(ILogger logger)
{
new Thread(() -> {
final String version = IOUtils.getLatestArtifactVersion("https://repo1.maven.org/maven2/fr/flowarg/flowupdater/maven-metadata.xml");
if (version == null)
{
logger.err("Couldn't get the latest version of FlowUpdater.");
logger.err("Maybe the maven repository is down? Or your internet connection sucks?");
return;
}
final int compare = Version.gen(FlowUpdater.FU_VERSION).compareTo(Version.gen(version));
if(compare > 0)
{
logger.info("You're running on an unpublished version of FlowUpdater. Are you in a dev environment?");
return;
}
if(compare < 0)
logger.warn(String.format("Detected a new version of FlowUpdater (%s). You should update!", version));
}).start();
}
}
================================================
FILE: src/main/java/fr/flowarg/flowupdater/utils/builderapi/BuilderArgument.java
================================================
package fr.flowarg.flowupdater.utils.builderapi;
import org.jetbrains.annotations.NotNull;
import java.util.function.Supplier;
/**
* Builder API; Represent an argument for a Builder implementation.
* @version 1.6
* @author flow
*
* @param Object Argument
*/
public class BuilderArgument
{
private final String objectName;
private T badObject = null;
private T object = null;
private boolean isRequired;
/**
* Construct a new BuilderArgument.
* @param objectName The name of the object.
* @param initialValue The initial value's wrapper.
*/
public BuilderArgument(String objectName, @NotNull Supplier initialValue)
{
this.objectName = objectName;
this.object = initialValue.get();
}
/**
* Construct a new basic BuilderArgument.
* @param objectName The name of the object.
*/
public BuilderArgument(String objectName)
{
this.objectName = objectName;
}
/**
* Construct a new BuilderArgument.
* @param objectName The name of the object.
* @param initialValue The initial value's wrapper.
* @param badObject The initial bad value's wrapper.
*/
public BuilderArgument(String objectName, @NotNull Supplier initialValue, @NotNull Supplier badObject)
{
this.objectName = objectName;
this.object = initialValue.get();
this.badObject = badObject.get();
}
/**
* Construct a new BuilderArgument.
* @param badObject The initial bad value's wrapper.
* @param objectName The name of the object.
*/
public BuilderArgument(@NotNull Supplier badObject, String objectName)
{
this.objectName = objectName;
this.badObject = badObject.get();
}
/**
* Check and get the wrapped object.
* @return the wrapper object.
* @throws BuilderException it the builder configuration is invalid.
*/
public T get() throws BuilderException
{
if(this.object == this.badObject && this.badObject != null)
throw new BuilderException("Argument" + this.objectName + " is a bad object!");
if(this.isRequired)
{
if(this.object == null)
throw new BuilderException("Argument" + this.objectName + " is null!");
else return this.object;
}
else return this.object;
}
/**
* Define the new wrapped object.
* @param object the new wrapper object to define.
*/
public void set(T object)
{
this.object = object;
}
/**
* Indicate that provided arguments are required if this argument is built.
* @param required required arguments.
* @return this.
*/
public BuilderArgument require(BuilderArgument> @NotNull ... required)
{
for (BuilderArgument> arg : required)
arg.isRequired = true;
return this;
}
/**
* Indicate that argument is required.
* @return this.
*/
public BuilderArgument required()
{
this.isRequired = true;
return this;
}
/**
* Indicate that argument is optional.
* @return this.
*/
public BuilderArgument optional()
{
this.isRequired = false;
return this;
}
/**
* Get the name of the current object's name.
* @return the object's name.
*/
public String getObjectName()
{
return this.objectName;
}
/**
* Get the bad object.
* @return the bad object.
*/
public T badObject()
{
return this.badObject;
}
@Override
public String toString()
{
return "BuilderArgument{" + "objectName='" + this.objectName + '\'' + ", isRequired=" + this.isRequired + '}';
}
}
================================================
FILE: src/main/java/fr/flowarg/flowupdater/utils/builderapi/BuilderException.java
================================================
package fr.flowarg.flowupdater.utils.builderapi;
/**
* Builder API; This exception is thrown when an error occurred with Builder API.
* @version 1.6
* @author flow
*/
public class BuilderException extends RuntimeException
{
private static final long serialVersionUID = 1L;
public BuilderException()
{
super();
}
public BuilderException(String reason)
{
super(reason);
}
public BuilderException(String reason, Throwable cause)
{
super(reason, cause);
}
public BuilderException(Throwable cause)
{
super(cause);
}
}
================================================
FILE: src/main/java/fr/flowarg/flowupdater/utils/builderapi/IBuilder.java
================================================
package fr.flowarg.flowupdater.utils.builderapi;
/**
* Builder API ; Builder interface.
* @version 1.6
* @author flow
*
* @param Object returned.
*/
@FunctionalInterface
public interface IBuilder
{
/**
* Build a {@link T} object.
* @return a {@link T} object.
* @throws BuilderException if an error occurred when building an object.
*/
T build() throws BuilderException;
}
================================================
FILE: src/main/java/fr/flowarg/flowupdater/utils/builderapi/package-info.java
================================================
/**
* Builder API package.
*/
package fr.flowarg.flowupdater.utils.builderapi;
================================================
FILE: src/main/java/fr/flowarg/flowupdater/utils/package-info.java
================================================
/**
* Utility package.
*/
package fr.flowarg.flowupdater.utils;
================================================
FILE: src/main/java/fr/flowarg/flowupdater/versions/AbstractModLoaderVersion.java
================================================
package fr.flowarg.flowupdater.versions;
import fr.flowarg.flowlogger.ILogger;
import fr.flowarg.flowupdater.FlowUpdater;
import fr.flowarg.flowupdater.download.DownloadList;
import fr.flowarg.flowupdater.download.IProgressCallback;
import fr.flowarg.flowupdater.download.Step;
import fr.flowarg.flowupdater.download.json.*;
import fr.flowarg.flowupdater.integrations.curseforgeintegration.ICurseForgeCompatible;
import fr.flowarg.flowupdater.integrations.modrinthintegration.IModrinthCompatible;
import fr.flowarg.flowupdater.integrations.modrinthintegration.ModrinthModPack;
import fr.flowarg.flowupdater.integrations.optifineintegration.IOptiFineCompatible;
import fr.flowarg.flowupdater.integrations.optifineintegration.OptiFine;
import fr.flowarg.flowupdater.utils.IOUtils;
import fr.flowarg.flowupdater.utils.ModFileDeleter;
import org.jetbrains.annotations.NotNull;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
public abstract class AbstractModLoaderVersion implements IModLoaderVersion, ICurseForgeCompatible, IModrinthCompatible, IOptiFineCompatible
{
protected final List mods;
protected final List curseMods;
protected final List modrinthMods;
protected final ModFileDeleter fileDeleter;
protected final CurseModPackInfo curseModPackInfo;
protected final ModrinthModPackInfo modrinthModPackInfo;
protected final OptiFineInfo optiFineInfo;
protected String modLoaderVersion;
protected ILogger logger;
protected VanillaVersion vanilla;
protected DownloadList downloadList;
protected IProgressCallback callback;
protected String javaPath;
protected ModrinthModPack modrinthModPack;
public AbstractModLoaderVersion(String modLoaderVersion, List mods, List curseMods,
List modrinthMods, ModFileDeleter fileDeleter, CurseModPackInfo curseModPackInfo,
ModrinthModPackInfo modrinthModPackInfo, OptiFineInfo optiFineInfo)
{
this.modLoaderVersion = modLoaderVersion;
this.mods = mods;
this.curseMods = curseMods;
this.modrinthMods = modrinthMods;
this.fileDeleter = fileDeleter;
this.curseModPackInfo = curseModPackInfo;
this.modrinthModPackInfo = modrinthModPackInfo;
this.optiFineInfo = optiFineInfo;
}
/**
* {@inheritDoc}
*/
@Override
public void attachFlowUpdater(@NotNull FlowUpdater flowUpdater)
{
this.logger = flowUpdater.getLogger();
this.vanilla = flowUpdater.getVanillaVersion();
this.downloadList = flowUpdater.getDownloadList();
this.callback = flowUpdater.getCallback();
this.javaPath = flowUpdater.getUpdaterOptions().getJavaPath();
}
/**
* {@inheritDoc}
*/
@Override
public List getMods()
{
return this.mods;
}
/**
* {@inheritDoc}
*/
@Override
public DownloadList getDownloadList()
{
return this.downloadList;
}
/**
* {@inheritDoc}
*/
@Override
public IProgressCallback getCallback()
{
return this.callback;
}
/**
* {@inheritDoc}
*/
@Override
public List getCurseMods()
{
return this.curseMods;
}
/**
* {@inheritDoc}
*/
@Override
public List getModrinthMods()
{
return this.modrinthMods;
}
/**
* {@inheritDoc}
*/
@Override
public void setAllCurseMods(List allCurseMods)
{
this.mods.addAll(allCurseMods);
}
/**
* {@inheritDoc}
*/
@Override
public CurseModPackInfo getCurseModPackInfo()
{
return this.curseModPackInfo;
}
/**
* {@inheritDoc}
*/
@Override
public ModrinthModPackInfo getModrinthModPackInfo()
{
return this.modrinthModPackInfo;
}
/**
* {@inheritDoc}
*/
@Override
public OptiFineInfo getOptiFineInfo()
{
return this.optiFineInfo;
}
/**
* {@inheritDoc}
*/
@Override
public void setAllModrinthMods(List modrinthMods)
{
this.mods.addAll(modrinthMods);
}
/**
* {@inheritDoc}
*/
@Override
public ILogger getLogger()
{
return this.logger;
}
/**
* {@inheritDoc}
*/
@Override
public String getModLoaderVersion()
{
return this.modLoaderVersion;
}
/**
* {@inheritDoc}
*/
@Override
public ModFileDeleter getFileDeleter()
{
return this.fileDeleter;
}
@Override
public void setModrinthModPack(ModrinthModPack modrinthModPack)
{
this.modrinthModPack = modrinthModPack;
}
@Override
public ModrinthModPack getModrinthModPack()
{
return this.modrinthModPack;
}
@Override
public void installMods(@NotNull Path modsDir) throws Exception
{
this.callback.step(Step.MODS);
this.installAllMods(modsDir);
final OptiFine ofObj = this.downloadList.getOptiFine();
if(ofObj != null)
{
try
{
final Path optiFineFilePath = modsDir.resolve(ofObj.getName());
if (Files.notExists(optiFineFilePath) || Files.size(optiFineFilePath) != ofObj.getSize())
IOUtils.copy(this.logger, modsDir.getParent().resolve(".op").resolve(ofObj.getName()), optiFineFilePath);
} catch (Exception e)
{
this.logger.printStackTrace(e);
}
this.downloadList.incrementDownloaded(ofObj.getSize());
this.callback.update(this.downloadList.getDownloadInfo());
}
this.fileDeleter.delete(this.logger, modsDir, this.mods, ofObj, this.modrinthModPack);
}
}
================================================
FILE: src/main/java/fr/flowarg/flowupdater/versions/IModLoaderVersion.java
================================================
package fr.flowarg.flowupdater.versions;
import fr.flowarg.flowlogger.ILogger;
import fr.flowarg.flowupdater.FlowUpdater;
import fr.flowarg.flowupdater.download.DownloadList;
import fr.flowarg.flowupdater.download.IProgressCallback;
import fr.flowarg.flowupdater.download.Step;
import fr.flowarg.flowupdater.download.json.Mod;
import fr.flowarg.flowupdater.utils.IOUtils;
import fr.flowarg.flowupdater.utils.ModFileDeleter;
import org.jetbrains.annotations.NotNull;
import java.net.URL;
import java.nio.file.Path;
import java.util.List;
public interface IModLoaderVersion
{
/**
* Attach {@link FlowUpdater} object to mod loaders, allow them to retrieve some information.
* @param flowUpdater flow updater object.
*/
void attachFlowUpdater(@NotNull FlowUpdater flowUpdater);
/**
* Check if the current mod loader is already installed.
* @param installDir the dir to check.
* @return if the current mod loader is already installed.
*/
boolean isModLoaderAlreadyInstalled(@NotNull Path installDir);
/**
* Install the current mod loader in a specified directory.
* @param installDir folder where the mod loader is going to be installed.
* @throws Exception if an I/O error occurred.
*/
default void install(@NotNull Path installDir) throws Exception
{
this.getCallback().step(Step.MOD_LOADER);
this.getLogger().info("Installing " + this.name() + ", version: " + this.getModLoaderVersion() + "...");
}
/**
* Install all mods in the mods' directory.
* @param modsDir mods directory.
* @throws Exception if an I/O error occurred.
*/
void installMods(@NotNull Path modsDir) throws Exception;
/**
* Get the mod loader version.
* @return the mod loader version.
*/
String getModLoaderVersion();
/**
* Get all processed mods / mods to process.
* @return all processed mods / mods to process.
*/
List getMods();
/**
* Download mods in the mods' folder.
* @param modsDir mods' folder
*/
default void installAllMods(@NotNull Path modsDir)
{
this.getDownloadList().getMods().forEach(mod -> {
try
{
final Path modFilePath = modsDir.resolve(mod.getName());
IOUtils.download(this.getLogger(), new URL(mod.getDownloadURL()), modFilePath);
this.getCallback().onFileDownloaded(modFilePath);
}
catch (Exception e)
{
this.getLogger().printStackTrace(e);
}
this.getDownloadList().incrementDownloaded(mod.getSize());
this.getCallback().update(this.getDownloadList().getDownloadInfo());
});
}
/**
* Get the {@link DownloadList} object.
* @return download info.
*/
DownloadList getDownloadList();
/**
* Get the {@link ILogger} object.
* @return the logger.
*/
ILogger getLogger();
/**
* Get the {@link IProgressCallback} object.
* @return the progress callback.
*/
IProgressCallback getCallback();
/**
* Get the attached {@link ModFileDeleter} instance;
* @return this mod file deleter;
*/
ModFileDeleter getFileDeleter();
/**
* Get the mod loader name.
* @return the mod loader name.
*/
String name();
}
================================================
FILE: src/main/java/fr/flowarg/flowupdater/versions/ModLoaderUtils.java
================================================
package fr.flowarg.flowupdater.versions;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import fr.flowarg.flowio.FileUtils;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.List;
public class ModLoaderUtils
{
@Contract(pure = true)
public static @NotNull String buildJarUrl(String baseUrl, @NotNull String group, String artifact, String version)
{
return buildJarUrl(baseUrl, group, artifact, version, "");
}
@Contract(pure = true)
public static @NotNull String buildJarUrl(String baseUrl, @NotNull String group, String artifact, String version, String classifier)
{
return baseUrl + group.replace(".", "/") + "/" + artifact + "/" + version + "/" + artifact + "-" + version + classifier + ".jar";
}
public static @NotNull Path buildLibraryPath(@NotNull Path installDir, @NotNull String group, String artifact, String version)
{
return installDir.resolve("libraries")
.resolve(group.replace(".", installDir.getFileSystem().getSeparator()))
.resolve(artifact)
.resolve(version)
.resolve(artifact + "-" + version + ".jar");
}
public static void fakeContext(@NotNull Path dirToInstall, String vanilla) throws Exception
{
final Path fakeProfiles = dirToInstall.resolve("launcher_profiles.json");
Files.write(fakeProfiles, "{}".getBytes(StandardCharsets.UTF_8));
final Path versions = dirToInstall.resolve("versions");
if(Files.notExists(versions))
Files.createDirectories(versions);
final Path vanillaVersion = versions.resolve(vanilla);
if(Files.notExists(vanillaVersion))
Files.createDirectories(vanillaVersion);
Files.copy(
dirToInstall.resolve("client.jar"),
vanillaVersion.resolve(vanilla + ".jar"),
StandardCopyOption.REPLACE_EXISTING
);
}
public static void removeFakeContext(@NotNull Path dirToInstall) throws Exception
{
FileUtils.deleteDirectory(dirToInstall.resolve("versions"));
Files.deleteIfExists(dirToInstall.resolve("launcher_profiles.json"));
}
public static @NotNull List parseNewVersionInfo(Path installDir, @NotNull JsonObject versionInfo) throws Exception
{
final List parsedLibraries = new ArrayList<>();
final JsonArray libraries = versionInfo.getAsJsonArray("libraries");
for (final JsonElement libraryElement : libraries)
{
final JsonObject library = libraryElement.getAsJsonObject();
final String name = library.get("name").getAsString();
final JsonObject downloads = library.getAsJsonObject("downloads");
final JsonObject artifact = downloads.getAsJsonObject("artifact");
final String path = artifact.get("path").getAsString();
final String sha1 = artifact.get("sha1").getAsString();
final String url = artifact.get("url").getAsString();
final Path libraryPath = installDir.resolve("libraries")
.resolve(path.replace("/", installDir.getFileSystem().getSeparator()));
final boolean installed = Files.exists(libraryPath) &&
FileUtils.getSHA1(libraryPath).equalsIgnoreCase(sha1);
parsedLibraries.add(new ParsedLibrary(libraryPath, url.isEmpty() ? null : new URL(url), name, installed));
}
return parsedLibraries;
}
}
================================================
FILE: src/main/java/fr/flowarg/flowupdater/versions/ModLoaderVersionBuilder.java
================================================
package fr.flowarg.flowupdater.versions;
import fr.flowarg.flowupdater.download.json.*;
import fr.flowarg.flowupdater.utils.ModFileDeleter;
import fr.flowarg.flowupdater.utils.builderapi.BuilderArgument;
import fr.flowarg.flowupdater.utils.builderapi.BuilderException;
import fr.flowarg.flowupdater.utils.builderapi.IBuilder;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
@SuppressWarnings("unchecked")
public abstract class ModLoaderVersionBuilder> implements IBuilder
{
protected final BuilderArgument> modsArgument = new BuilderArgument>("Mods", ArrayList::new).optional();
protected final BuilderArgument> curseModsArgument = new BuilderArgument>("CurseMods", ArrayList::new).optional();
protected final BuilderArgument> modrinthModsArgument = new BuilderArgument>("ModrinthMods", ArrayList::new).optional();
protected final BuilderArgument fileDeleterArgument = new BuilderArgument<>("ModFileDeleter", () -> new ModFileDeleter(false)).optional();
protected final BuilderArgument curseModPackArgument = new BuilderArgument("CurseModPack").optional();
protected final BuilderArgument modrinthPackArgument = new BuilderArgument("ModrinthModPack").optional();
/**
* Append a mod list to the version.
* @param mods mods to append.
* @return the builder.
*/
public B withMods(List mods)
{
this.modsArgument.get().addAll(mods);
return (B) this;
}
/**
* Append a single mod or a mod array to the version.
* @param mods mods to append.
* @return the builder.
*/
public B withMods(Mod... mods)
{
return this.withMods(Arrays.asList(mods));
}
/**
* Append mods contained in the provided JSON url.
* @param jsonUrl The json URL of mods to append.
* @return the builder.
*/
public B withMods(URL jsonUrl)
{
return this.withMods(Mod.getModsFromJson(jsonUrl));
}
/**
* Append mods contained in the provided JSON url.
* @param jsonUrl The json URL of mods to append.
* @return the builder.
*/
public B withMods(String jsonUrl)
{
return this.withMods(Mod.getModsFromJson(jsonUrl));
}
/**
* Append a mod list to the version.
* @param curseMods CurseForge's mods to append.
* @return the builder.
*/
public B withCurseMods(Collection curseMods)
{
this.curseModsArgument.get().addAll(curseMods);
return (B) this;
}
/**
* Append a single mod or a mod array to the version.
* @param curseMods CurseForge's mods to append.
* @return the builder.
*/
public B withCurseMods(CurseFileInfo... curseMods)
{
return this.withCurseMods(Arrays.asList(curseMods));
}
/**
* Append mods contained in the provided JSON url.
* @param jsonUrl The json URL of mods to append.
* @return the builder.
*/
public B withCurseMods(URL jsonUrl)
{
return this.withCurseMods(CurseFileInfo.getFilesFromJson(jsonUrl));
}
/**
* Append mods contained in the provided JSON url.
* @param jsonUrl The json URL of mods to append.
* @return the builder.
*/
public B withCurseMods(String jsonUrl)
{
return this.withCurseMods(CurseFileInfo.getFilesFromJson(jsonUrl));
}
/**
* Append a mod list to the version.
* @param modrinthMods Modrinth's mods to append.
* @return the builder.
*/
public B withModrinthMods(Collection modrinthMods)
{
this.modrinthModsArgument.get().addAll(modrinthMods);
return (B) this;
}
/**
* Append a single mod or a mod array to the version.
* @param modrinthMods Modrinth's mods to append.
* @return the builder.
*/
public B withModrinthMods(ModrinthVersionInfo... modrinthMods)
{
return this.withModrinthMods(Arrays.asList(modrinthMods));
}
/**
* Append mods contained in the provided JSON url.
* @param jsonUrl The json URL of mods to append.
* @return the builder.
*/
public B withModrinthMods(URL jsonUrl)
{
return this.withModrinthMods(ModrinthVersionInfo.getModrinthVersionsFromJson(jsonUrl));
}
/**
* Append mods contained in the provided JSON url.
* @param jsonUrl The json URL of mods to append.
* @return the builder.
*/
public B withModrinthMods(String jsonUrl)
{
return this.withModrinthMods(ModrinthVersionInfo.getModrinthVersionsFromJson(jsonUrl));
}
/**
* Assign to the future forge version a mod pack.
* @param modPackInfo the mod pack information to assign.
* @return the builder.
*/
public B withCurseModPack(CurseModPackInfo modPackInfo)
{
this.curseModPackArgument.set(modPackInfo);
return (B) this;
}
/**
* Assign to the future forge version a mod pack.
* @param modPackInfo the mod pack information to assign.
* @return the builder.
*/
public B withModrinthModPack(ModrinthModPackInfo modPackInfo)
{
this.modrinthPackArgument.set(modPackInfo);
return (B) this;
}
/**
* Append a file deleter to the version.
* @param fileDeleter the file deleter to append.
* @return the builder.
*/
public B withFileDeleter(ModFileDeleter fileDeleter)
{
this.fileDeleterArgument.set(fileDeleter);
return (B) this;
}
@Override
public abstract T build() throws BuilderException;
}
================================================
FILE: src/main/java/fr/flowarg/flowupdater/versions/ParsedLibrary.java
================================================
package fr.flowarg.flowupdater.versions;
import fr.flowarg.flowlogger.ILogger;
import fr.flowarg.flowupdater.utils.IOUtils;
import java.net.URL;
import java.nio.file.Path;
import java.util.Optional;
public class ParsedLibrary
{
private final Path path;
private final URL url;
private final String artifact;
private final boolean installed;
public ParsedLibrary(Path path, URL url, String artifact, boolean installed)
{
this.path = path;
this.url = url;
this.artifact = artifact;
this.installed = installed;
}
public void download(ILogger logger)
{
if(this.url != null)
IOUtils.download(logger, this.url, this.path);
}
public Path getPath()
{
return this.path;
}
public Optional getUrl()
{
return Optional.ofNullable(this.url);
}
public String getArtifact()
{
return this.artifact;
}
public boolean isInstalled()
{
return this.installed;
}
}
================================================
FILE: src/main/java/fr/flowarg/flowupdater/versions/VanillaVersion.java
================================================
package fr.flowarg.flowupdater.versions;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import fr.flowarg.flowstringer.StringUtils;
import fr.flowarg.flowupdater.FlowUpdater;
import fr.flowarg.flowupdater.download.json.AssetDownloadable;
import fr.flowarg.flowupdater.download.json.AssetIndex;
import fr.flowarg.flowupdater.download.json.Downloadable;
import fr.flowarg.flowupdater.download.json.MCP;
import fr.flowarg.flowupdater.utils.IOUtils;
import fr.flowarg.flowupdater.utils.builderapi.BuilderArgument;
import fr.flowarg.flowupdater.utils.builderapi.BuilderException;
import fr.flowarg.flowupdater.utils.builderapi.IBuilder;
import org.jetbrains.annotations.NotNull;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
public class VanillaVersion
{
/**
* Default version. It used when an update doesn't need a Minecraft installation.
*/
public static final VanillaVersion NULL_VERSION = new VanillaVersion("no", null, false,
null, new ArrayList<>(),
new ArrayList<>(), null);
private final String name;
private final MCP mcp;
private final boolean snapshot;
private final AssetIndex customAssetIndex;
private final List anotherAssets;
private final List anotherLibraries;
private final boolean custom;
private JsonElement json = null;
private String jsonURL = null;
private VanillaVersion(String name, MCP mcp,
boolean snapshot,
AssetIndex customAssetIndex, List anotherAssets,
List anotherLibraries, JsonObject customVersionJson)
{
this.name = name;
this.mcp = mcp;
this.snapshot = snapshot;
this.customAssetIndex = customAssetIndex;
this.anotherAssets = anotherAssets;
this.anotherLibraries = anotherLibraries;
this.custom = customVersionJson != null;
if(!this.name.equals("no"))
this.json = (customVersionJson == null ? IOUtils.readJson(this.getJsonVersion()) : customVersionJson);
}
/**
* Get the JSON array representing all Minecraft's libraries.
* @return the libraries in JSON format.
*/
public JsonArray getMinecraftLibrariesJson()
{
return this.json.getAsJsonObject().getAsJsonArray("libraries");
}
/**
* Get the JSON object representing Minecraft's client.
* @return the client in JSON format.
*/
public JsonObject getMinecraftClient()
{
if(!this.custom && this.mcp != null)
{
final JsonObject result = new JsonObject();
final String sha1 = this.mcp.getClientSha1();
final String url = this.mcp.getClientURL();
final long size = this.mcp.getClientSize();
if(StringUtils.checkString(sha1) && StringUtils.checkString(url) && size > 0)
{
result.addProperty("sha1", sha1);
result.addProperty("size", size);
result.addProperty("url", url);
return result;
}
else FlowUpdater.DEFAULT_LOGGER.warn("Skipped MCP Client");
}
return this.json.getAsJsonObject().getAsJsonObject("downloads").getAsJsonObject("client");
}
/**
* Get the JSON object representing Minecraft's asset index.
* @return the asset index in JSON format.
*/
public JsonObject getMinecraftAssetIndex()
{
return this.json.getAsJsonObject().getAsJsonObject("assetIndex");
}
/**
* Get the input stream of the wanted version json.
*/
private InputStream getJsonVersion()
{
final AtomicReference version = new AtomicReference<>(this.getName());
final AtomicReference result = new AtomicReference<>(null);
try
{
final JsonObject launcherMeta = IOUtils.readJson(
new URL("https://piston-meta.mojang.com/mc/game/version_manifest_v2.json")
.openStream())
.getAsJsonObject();
if (this.getName().equals("latest"))
{
final JsonObject latest = launcherMeta.getAsJsonObject("latest");
if (this.snapshot)
version.set(latest.get("snapshot").getAsString());
else version.set(latest.get("release").getAsString());
}
launcherMeta.getAsJsonArray("versions").forEach(jsonElement ->
{
if (!jsonElement.getAsJsonObject().get("id").getAsString().equals(version.get())) return;
try
{
this.jsonURL = jsonElement.getAsJsonObject().get("url").getAsString();
result.set(new URL(this.jsonURL).openStream());
} catch (Exception e)
{
FlowUpdater.DEFAULT_LOGGER.printStackTrace(e);
}
});
} catch (Exception e)
{
FlowUpdater.DEFAULT_LOGGER.printStackTrace(e);
}
return result.get();
}
/**
* Get the name of the version.
* @return the name of the version.
*/
public @NotNull String getName()
{
return this.name;
}
/**
* Get the MCP object of the version.
* @return the MCP object of the version.
*/
public MCP getMCP()
{
return this.mcp;
}
/**
* Is the current version a snapshot?
* @return if the current version is a snapshot.
*/
public boolean isSnapshot()
{
return this.snapshot;
}
/**
* The custom asset index.
* @return the custom asset index.
*/
public AssetIndex getCustomAssetIndex()
{
return this.customAssetIndex;
}
/**
* The list of custom assets.
* @return The list of custom assets.
*/
public List getAnotherAssets()
{
return this.anotherAssets;
}
/**
* The list of custom libraries.
* @return The list of custom libraries.
*/
public List getAnotherLibraries()
{
return this.anotherLibraries;
}
/**
* Get the url of the JSON version.
* @return the url of the JSON version.
*/
public String getJsonURL()
{
return this.jsonURL;
}
/**
* A builder for building a vanilla version like {@link FlowUpdater.FlowUpdaterBuilder}
* @author FlowArg
*/
public static class VanillaVersionBuilder implements IBuilder
{
private final BuilderArgument nameArgument = new BuilderArgument("Name").required();
private final BuilderArgument mcpArgument = new BuilderArgument("MCP").optional();
private final BuilderArgument snapshotArgument = new BuilderArgument<>("Snapshot", () -> false).optional();
private final BuilderArgument customAssetIndexArgument = new BuilderArgument("CustomAssetIndex").optional();
private final BuilderArgument> anotherAssetsArgument = new BuilderArgument>("AnotherAssets", ArrayList::new).optional();
private final BuilderArgument> anotherLibrariesArgument = new BuilderArgument>("AnotherLibraries", ArrayList::new).optional();
private final BuilderArgument customVersionJsonArgument = new BuilderArgument("CustomVersionJson").optional();
/**
* Define the name of the wanted Minecraft version.
* @param name wanted Minecraft's version.
* @return the builder.
*/
public VanillaVersionBuilder withName(String name)
{
this.nameArgument.set(name);
return this;
}
/**
* Append a mcp object to the version
* @param mcp the mcp object to append.
* @return the builder.
*/
public VanillaVersionBuilder withMCP(MCP mcp)
{
this.mcpArgument.set(mcp);
return this;
}
/**
* Append a mcp object to the version
* @param mcpJsonUrl the mcp json url of mcp object to append.
* @return the builder.
*/
public VanillaVersionBuilder withMCP(URL mcpJsonUrl)
{
return withMCP(MCP.getMCPFromJson(mcpJsonUrl));
}
/**
* Append a mcp object to the version
* @param mcpJsonUrl the mcp json url of mcp object to append.
* @return the builder.
*/
public VanillaVersionBuilder withMCP(String mcpJsonUrl)
{
return withMCP(MCP.getMCPFromJson(mcpJsonUrl));
}
/**
* Required if you want the latest snapshot version. Otherwise, it's unnecessary.
* @param snapshot if the version is a snapshot.
* @return the builder.
*/
public VanillaVersionBuilder withSnapshot(boolean snapshot)
{
this.snapshotArgument.set(snapshot);
return this;
}
/**
* Add custom asset index to the version.
* @param assetIndex the custom asset index to add.
* @return the builder.
*/
public VanillaVersionBuilder withCustomAssetIndex(AssetIndex assetIndex)
{
this.customAssetIndexArgument.set(assetIndex);
return this;
}
/**
* Add custom assets to the version.
* @param anotherAssets custom assets to add.
* @return the builder.
*/
public VanillaVersionBuilder withAnotherAssets(Collection anotherAssets)
{
this.anotherAssetsArgument.get().addAll(anotherAssets);
return this;
}
/**
* Add custom assets to the version.
* @param anotherAssets custom assets to add.
* @return the builder.
*/
public VanillaVersionBuilder withAnotherAssets(AssetDownloadable... anotherAssets)
{
return withAnotherAssets(Arrays.asList(anotherAssets));
}
/**
* Add custom libraries to the version.
* @param anotherLibraries custom libraries to add.
* @return the builder.
*/
public VanillaVersionBuilder withAnotherLibraries(Collection anotherLibraries)
{
this.anotherLibrariesArgument.get().addAll(anotherLibraries);
return this;
}
/**
* Add custom libraries to the version.
* @param anotherLibraries custom libraries to add.
* @return the builder.
*/
public VanillaVersionBuilder withAnotherLibraries(Downloadable... anotherLibraries)
{
return withAnotherLibraries(Arrays.asList(anotherLibraries));
}
/**
* Define the version's json.
* @param customVersionJson the custom version's json to set.
* @return the builder.
*/
public VanillaVersionBuilder withCustomVersionJson(JsonObject customVersionJson)
{
this.customVersionJsonArgument.set(customVersionJson);
return this;
}
/**
* Define the version's json.
* @param customVersionJsonUrl the custom version's json url to set.
* @return the builder.
*/
public VanillaVersionBuilder withCustomVersionJson(URL customVersionJsonUrl)
{
this.customVersionJsonArgument.set(IOUtils.readJson(customVersionJsonUrl).getAsJsonObject());
return this;
}
/**
* Build a new {@link VanillaVersion} instance with provided arguments.
* @return the freshly created instance.
* @throws BuilderException if an error occurred.
*/
@Override
public VanillaVersion build() throws BuilderException
{
return new VanillaVersion(
this.nameArgument.get(),
this.mcpArgument.get(),
this.snapshotArgument.get(),
this.customAssetIndexArgument.get(),
this.anotherAssetsArgument.get(),
this.anotherLibrariesArgument.get(),
this.customVersionJsonArgument.get()
);
}
}
}
================================================
FILE: src/main/java/fr/flowarg/flowupdater/versions/fabric/FabricBasedVersion.java
================================================
package fr.flowarg.flowupdater.versions.fabric;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import fr.flowarg.flowio.FileUtils;
import fr.flowarg.flowupdater.download.json.*;
import fr.flowarg.flowupdater.utils.IOUtils;
import fr.flowarg.flowupdater.utils.ModFileDeleter;
import fr.flowarg.flowupdater.versions.AbstractModLoaderVersion;
import fr.flowarg.flowupdater.versions.ModLoaderUtils;
import fr.flowarg.flowupdater.versions.ParsedLibrary;
import org.jetbrains.annotations.NotNull;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
public abstract class FabricBasedVersion extends AbstractModLoaderVersion
{
protected final String metaApi;
protected String versionId;
public FabricBasedVersion(String modLoaderVersion, List mods, List curseMods,
List modrinthMods, ModFileDeleter fileDeleter, CurseModPackInfo curseModPackInfo,
ModrinthModPackInfo modrinthModPackInfo, OptiFineInfo optiFineInfo, String metaApi)
{
super(modLoaderVersion, mods, curseMods, modrinthMods, fileDeleter, curseModPackInfo, modrinthModPackInfo, optiFineInfo);
this.metaApi = metaApi;
}
@Override
public boolean isModLoaderAlreadyInstalled(@NotNull Path installDir)
{
final Path versionJsonFile = installDir.resolve(this.versionId + ".json");
if(Files.notExists(versionJsonFile))
return false;
try {
return this.parseLibraries(versionJsonFile, installDir).stream().allMatch(ParsedLibrary::isInstalled);
}
catch (Exception e)
{
this.logger.err("An error occurred while checking if the mod loader is already installed.");
return false;
}
}
/**
* {@inheritDoc}
*/
@Override
public void install(final @NotNull Path installDir) throws Exception
{
super.install(installDir);
final Path versionJsonFile = installDir.resolve(this.versionId + ".json");
IOUtils.download(this.logger, new URL(String.format(this.metaApi, this.vanilla.getName(), this.modLoaderVersion)), versionJsonFile);
try {
final List parsedLibraries = this.parseLibraries(versionJsonFile, installDir);
parsedLibraries.stream()
.filter(parsedLibrary -> !parsedLibrary.isInstalled())
.forEach(parsedLibrary -> parsedLibrary.download(this.logger));
}
catch (Exception e)
{
this.logger.err("An error occurred while installing the mod loader.");
}
}
protected List parseLibraries(Path versionJsonFile, Path installDir) throws Exception
{
final List parsedLibraries = new ArrayList<>();
final JsonObject object = JsonParser.parseReader(Files.newBufferedReader(versionJsonFile))
.getAsJsonObject();
final JsonArray libraries = object.getAsJsonArray("libraries");
for (final JsonElement libraryElement : libraries)
{
final JsonObject library = libraryElement.getAsJsonObject();
final String url = library.get("url").getAsString();
final String completeArtifact = library.get("name").getAsString();
final String[] name = completeArtifact.split(":");
final String group = name[0];
final String artifact = name[1];
final String version = name[2];
final String builtJarUrl = ModLoaderUtils.buildJarUrl(url, group, artifact, version);
final Path builtLibaryPath = ModLoaderUtils.buildLibraryPath(installDir, group, artifact, version);
final Callable sha1 = this.getSha1FromLibrary(library, builtJarUrl);
final boolean installed = Files.exists(builtLibaryPath) &&
FileUtils.getSHA1(builtLibaryPath).equalsIgnoreCase(sha1.call());
parsedLibraries.add(new ParsedLibrary(builtLibaryPath, new URL(builtJarUrl), completeArtifact, installed));
}
return parsedLibraries;
}
protected Callable getSha1FromLibrary(@NotNull JsonObject library, String builtJarUrl)
{
final JsonElement sha1Elem = library.get("sha1");
if (sha1Elem != null)
return sha1Elem::getAsString;
return () -> IOUtils.getContent(new URL(builtJarUrl + ".sha1"));
}
}
================================================
FILE: src/main/java/fr/flowarg/flowupdater/versions/fabric/FabricVersion.java
================================================
package fr.flowarg.flowupdater.versions.fabric;
import fr.flowarg.flowupdater.FlowUpdater;
import fr.flowarg.flowupdater.download.json.*;
import fr.flowarg.flowupdater.utils.ModFileDeleter;
import org.jetbrains.annotations.NotNull;
import java.util.List;
/**
* The object that contains Fabric's stuff.
*/
public class FabricVersion extends FabricBasedVersion
{
private static final String FABRIC_META_API = "https://meta.fabricmc.net/v2/versions/loader/%s/%s/profile/json";
/**
* Use {@link FabricVersionBuilder} to instantiate this class.
* @param mods {@link List} to install.
* @param curseMods {@link List} to install.
* @param fabricVersion to install.
* @param fileDeleter {@link ModFileDeleter} used to clean up mods' dir.
* @param curseModPackInfo {@link CurseModPackInfo} the mod pack you want to install.
*/
FabricVersion(String fabricVersion, List mods, List curseMods,
List modrinthMods, ModFileDeleter fileDeleter, CurseModPackInfo curseModPackInfo,
ModrinthModPackInfo modrinthModPackInfo, OptiFineInfo optiFineInfo)
{
super(fabricVersion, mods, curseMods, modrinthMods, fileDeleter, curseModPackInfo, modrinthModPackInfo, optiFineInfo, FABRIC_META_API);
}
@Override
public void attachFlowUpdater(@NotNull FlowUpdater flowUpdater)
{
super.attachFlowUpdater(flowUpdater);
this.versionId = "fabric-loader-" + this.modLoaderVersion + "-" + this.vanilla.getName();
}
@Override
public String name()
{
return "Fabric";
}
}
================================================
FILE: src/main/java/fr/flowarg/flowupdater/versions/fabric/FabricVersionBuilder.java
================================================
package fr.flowarg.flowupdater.versions.fabric;
import fr.flowarg.flowupdater.download.json.OptiFineInfo;
import fr.flowarg.flowupdater.utils.IOUtils;
import fr.flowarg.flowupdater.utils.builderapi.BuilderArgument;
import fr.flowarg.flowupdater.utils.builderapi.BuilderException;
import fr.flowarg.flowupdater.versions.ModLoaderVersionBuilder;
public class FabricVersionBuilder extends ModLoaderVersionBuilder
{
private static final String FABRIC_VERSION_METADATA =
"https://maven.fabricmc.net/net/fabricmc/fabric-loader/maven-metadata.xml";
private final BuilderArgument fabricVersionArgument =
new BuilderArgument<>("FabricVersion", () ->
IOUtils.getLatestArtifactVersion(FABRIC_VERSION_METADATA))
.optional();
private final BuilderArgument optiFineArgument = new BuilderArgument("OptiFine").optional();
/**
* @param fabricVersion the Fabric version you want to install
* (don't use this function if you want to use the latest fabric version).
* @return the builder.
*/
public FabricVersionBuilder withFabricVersion(String fabricVersion)
{
this.fabricVersionArgument.set(fabricVersion);
return this;
}
/**
* Append some OptiFine download's information.
* @param optiFineInfo OptiFine info.
* @return the builder.
*/
public FabricVersionBuilder withOptiFine(OptiFineInfo optiFineInfo)
{
this.optiFineArgument.set(optiFineInfo);
return this;
}
/**
* Build a new {@link FabricVersion} instance with provided arguments.
* @return the freshly created instance.
* @throws BuilderException if an error occurred.
*/
@Override
public FabricVersion build() throws BuilderException
{
return new FabricVersion(
this.fabricVersionArgument.get(),
this.modsArgument.get(),
this.curseModsArgument.get(),
this.modrinthModsArgument.get(),
this.fileDeleterArgument.get(),
this.curseModPackArgument.get(),
this.modrinthPackArgument.get(),
this.optiFineArgument.get()
);
}
}
================================================
FILE: src/main/java/fr/flowarg/flowupdater/versions/fabric/QuiltVersion.java
================================================
package fr.flowarg.flowupdater.versions.fabric;
import fr.flowarg.flowupdater.FlowUpdater;
import fr.flowarg.flowupdater.download.json.*;
import fr.flowarg.flowupdater.utils.ModFileDeleter;
import org.jetbrains.annotations.NotNull;
import java.util.List;
/**
* The object that contains Quilt's stuff.
*/
public class QuiltVersion extends FabricBasedVersion
{
private static final String QUILT_META_API = "https://meta.quiltmc.org/v3/versions/loader/%s/%s/profile/json";
/**
* Use {@link QuiltVersionBuilder} to instantiate this class.
* @param mods {@link List} to install.
* @param curseMods {@link List} to install.
* @param quiltVersion to install.
* @param fileDeleter {@link ModFileDeleter} used to clean up mods' dir.
* @param curseModPackInfo {@link CurseModPackInfo} the mod pack you want to install.
*/
QuiltVersion(String quiltVersion, List mods, List curseMods,
List modrinthMods, ModFileDeleter fileDeleter, CurseModPackInfo curseModPackInfo,
ModrinthModPackInfo modrinthModPackInfo, OptiFineInfo optiFineInfo)
{
super(quiltVersion, mods, curseMods, modrinthMods, fileDeleter, curseModPackInfo, modrinthModPackInfo, optiFineInfo, QUILT_META_API);
}
@Override
public void attachFlowUpdater(@NotNull FlowUpdater flowUpdater)
{
super.attachFlowUpdater(flowUpdater);
this.versionId = "quilt-loader-" + this.modLoaderVersion + "-" + this.vanilla.getName();
}
@Override
public String name()
{
return "Quilt";
}
}
================================================
FILE: src/main/java/fr/flowarg/flowupdater/versions/fabric/QuiltVersionBuilder.java
================================================
package fr.flowarg.flowupdater.versions.fabric;
import fr.flowarg.flowupdater.download.json.OptiFineInfo;
import fr.flowarg.flowupdater.utils.IOUtils;
import fr.flowarg.flowupdater.utils.builderapi.BuilderArgument;
import fr.flowarg.flowupdater.utils.builderapi.BuilderException;
import fr.flowarg.flowupdater.versions.ModLoaderVersionBuilder;
public class QuiltVersionBuilder extends ModLoaderVersionBuilder
{
private static final String QUILT_VERSION_METADATA =
"https://maven.quiltmc.org/repository/release/org/quiltmc/quilt-loader/maven-metadata.xml";
private final BuilderArgument quiltVersionArgument =
new BuilderArgument<>("QuiltVersion", () -> IOUtils.getLatestArtifactVersion(QUILT_VERSION_METADATA)).optional();
private final BuilderArgument optiFineArgument = new BuilderArgument("OptiFine").optional();
/**
* @param quiltVersion the Quilt version you want to install
* (don't use this function if you want to use the latest Quilt version).
* @return the builder.
*/
public QuiltVersionBuilder withQuiltVersion(String quiltVersion)
{
this.quiltVersionArgument.set(quiltVersion);
return this;
}
/**
* Append some OptiFine download's information.
* @param optiFineInfo OptiFine info.
* @return the builder.
*/
public QuiltVersionBuilder withOptiFine(OptiFineInfo optiFineInfo)
{
this.optiFineArgument.set(optiFineInfo);
return this;
}
/**
* Build a new {@link QuiltVersion} instance with provided arguments.
* @return the freshly created instance.
* @throws BuilderException if an error occurred.
*/
@Override
public QuiltVersion build() throws BuilderException
{
return new QuiltVersion(
this.quiltVersionArgument.get(),
this.modsArgument.get(),
this.curseModsArgument.get(),
this.modrinthModsArgument.get(),
this.fileDeleterArgument.get(),
this.curseModPackArgument.get(),
this.modrinthPackArgument.get(),
this.optiFineArgument.get()
);
}
}
================================================
FILE: src/main/java/fr/flowarg/flowupdater/versions/fabric/package-info.java
================================================
/**
* This package contains all the classes that are used to install Fabric-based mod loaders (Fabric and Quilt at the moment).
*/
package fr.flowarg.flowupdater.versions.fabric;
================================================
FILE: src/main/java/fr/flowarg/flowupdater/versions/forge/ForgeVersion.java
================================================
package fr.flowarg.flowupdater.versions.forge;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import fr.flowarg.flowio.FileUtils;
import fr.flowarg.flowstringer.StringUtils;
import fr.flowarg.flowupdater.download.json.*;
import fr.flowarg.flowupdater.integrations.optifineintegration.IOptiFineCompatible;
import fr.flowarg.flowupdater.utils.IOUtils;
import fr.flowarg.flowupdater.utils.ModFileDeleter;
import fr.flowarg.flowupdater.utils.Version;
import fr.flowarg.flowupdater.versions.AbstractModLoaderVersion;
import fr.flowarg.flowupdater.versions.ModLoaderUtils;
import fr.flowarg.flowupdater.versions.ParsedLibrary;
import org.jetbrains.annotations.NotNull;
import java.net.URI;
import java.net.URL;
import java.nio.file.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.stream.Collectors;
public class ForgeVersion extends AbstractModLoaderVersion implements IOptiFineCompatible
{
private final OptiFineInfo optiFineInfo;
private final String versionId;
private final boolean shouldUseInstaller;
private final boolean newInstallerJsonSpec;
public ForgeVersion(String modLoaderVersion, List mods, List curseMods,
List modrinthMods, ModFileDeleter fileDeleter, CurseModPackInfo curseModPackInfo,
ModrinthModPackInfo modrinthModPackInfo, OptiFineInfo optiFineInfo)
{
super(modLoaderVersion, mods, curseMods, modrinthMods, fileDeleter, curseModPackInfo, modrinthModPackInfo, optiFineInfo);
this.optiFineInfo = optiFineInfo;
final String[] data = this.modLoaderVersion.split("-");
final String vanilla = data[0];
final String forge = data[1];
final Version vanillaVersion = Version.gen(vanilla);
if(vanillaVersion.isEqualTo(Version.gen("1.20.3")))
throw new IllegalArgumentException("Forge 1.20.3 is not supported. (can't even launch through official launcher!).");
final Version forgeVersion = Version.gen(forge);
if (data.length == 2)
{
if(forgeVersion.isNewerOrEqualTo(Version.gen("14.23.5.2851")))
{
this.versionId = vanilla + "-forge-" + forge;
this.shouldUseInstaller = vanillaVersion.isNewerThan(Version.gen("1.12.2"));
this.newInstallerJsonSpec = true;
}
else
{
this.versionId = vanilla + "-forge" + this.modLoaderVersion;
this.shouldUseInstaller = false;
this.newInstallerJsonSpec = false;
}
}
else
{
if(vanillaVersion.isOlderOrEqualTo(Version.gen("1.7.10")))
this.versionId = vanilla + "-Forge" + forge + "-" + data[2];
else this.versionId = vanilla + "-forge" + this.modLoaderVersion;
this.shouldUseInstaller = false;
this.newInstallerJsonSpec = false;
}
}
@Override
public boolean isModLoaderAlreadyInstalled(@NotNull Path installDir)
{
final Path versionJsonFile = installDir.resolve(this.versionId + ".json");
if(Files.notExists(versionJsonFile))
return false;
try {
final JsonObject object = JsonParser.parseReader(Files.newBufferedReader(versionJsonFile))
.getAsJsonObject();
if(this.newInstallerJsonSpec)
{
final String vanillaVersionStr = this.vanilla.getName();
final Version vanillaVersion = Version.gen(vanillaVersionStr);
final boolean firstPass = ModLoaderUtils.parseNewVersionInfo(installDir, object).stream().allMatch(ParsedLibrary::isInstalled);
if(vanillaVersion.isEqualTo(Version.gen("1.12.2")))
return firstPass;
if(!firstPass)
return false;
final Path librariesDir = installDir.resolve("libraries");
// 1.13.2 --> 1.15.2 = minecraft (vanilla : slim + extra) + (vanilla-mcp : srg)
// 1.16.1 --> 1.20.2 = minecraft (vanilla-mcp : slim + extra + srg)
if(vanillaVersion.isBetweenOrEqual(Version.gen("1.13.2"), Version.gen("1.15.2")))
{
final String mcpVersion = this.getMcpVersion(object);
final Path minecraftDir = librariesDir
.resolve("net")
.resolve("minecraft")
.resolve("client");
final Path vanillaDir = minecraftDir.resolve(vanillaVersionStr);
final Path vanillaMcpDir = minecraftDir.resolve(vanillaVersionStr + "-" + mcpVersion);
final Path extraJar = vanillaDir.resolve("client-" + vanillaVersionStr + "-extra.jar");
final Path extraJarCache = vanillaDir.resolve("client-" + vanillaVersionStr + "-extra.jar.cache");
final Path slimJar = vanillaDir.resolve("client-" + vanillaVersionStr + "-slim.jar");
final Path slimJarCache = vanillaDir.resolve("client-" + vanillaVersionStr + "-slim.jar.cache");
final Path srgJar = vanillaMcpDir.resolve("client-" + vanillaVersionStr + "-" + mcpVersion + "-srg.jar");
if (this.isSlimOrExtraSha1Wrong(extraJar, extraJarCache, slimJar, slimJarCache, srgJar))
return false;
}
else if(vanillaVersion.isBetweenOrEqual(Version.gen("1.16.1"), Version.gen("1.20.2")))
{
final String mcpVersion = this.getMcpVersion(object);
final String clientId = "client-" + vanillaVersionStr + "-" + mcpVersion;
final Path vanillaMcpDir = librariesDir
.resolve("net")
.resolve("minecraft")
.resolve("client")
.resolve(vanillaVersionStr + "-" + mcpVersion);
final Path extraJar = vanillaMcpDir.resolve(clientId + "-extra.jar");
final Path extraJarCache = vanillaMcpDir.resolve(clientId + "-extra.jar.cache");
final Path slimJar = vanillaMcpDir.resolve(clientId + "-slim.jar");
final Path slimJarCache = vanillaMcpDir.resolve(clientId + "-slim.jar.cache");
final Path srgJar = vanillaMcpDir.resolve(clientId + "-srg.jar");
if (this.isSlimOrExtraSha1Wrong(extraJar, extraJarCache, slimJar, slimJarCache, srgJar))
return false;
}
// 1.12.2 = libs
// 1.13.2 --> 1.20.2 = libs + client + universal
// 1.20.4 --> 1.21 = libs + shim
if(vanillaVersion.isBetweenOrEqual(Version.gen("1.13.2"), Version.gen("1.20.2")))
{
final Path forgeDir = librariesDir
.resolve("net")
.resolve("minecraftforge")
.resolve("forge")
.resolve(this.modLoaderVersion);
final Path universalJar = forgeDir.resolve("forge-" + this.modLoaderVersion + "-universal.jar");
final Path clientJar = forgeDir.resolve("forge-" + this.modLoaderVersion + "-client.jar");
if(Files.notExists(universalJar) || Files.notExists(clientJar))
return false;
}
else if(vanillaVersion.isNewerOrEqualTo(Version.gen("1.20.4")))
{
final Path shimJar = librariesDir
.resolve("net")
.resolve("minecraftforge")
.resolve("forge")
.resolve(this.modLoaderVersion)
.resolve("forge-" + this.modLoaderVersion + "-shim.jar");
if(Files.notExists(shimJar))
return false;
}
}
else return this.parseOldVersionInfo(installDir, object).stream().allMatch(ParsedLibrary::isInstalled);
}
catch (Exception e)
{
this.logger.err("An error occurred while checking if the mod loader is already installed.");
return false;
}
return true;
}
private String getMcpVersion(@NotNull JsonObject object)
{
final List gameArguments = object
.getAsJsonObject("arguments")
.getAsJsonArray("game")
.asList()
.stream()
.filter(JsonElement::isJsonPrimitive)
.map(JsonElement::getAsString)
.collect(Collectors.toList());
return gameArguments.get(gameArguments.indexOf("--fml.mcpVersion") + 1);
}
private boolean isSlimOrExtraSha1Wrong(Path extraJar, Path extraJarCache, Path slimJar, Path slimJarCache, Path srgJar) throws Exception
{
if(Files.notExists(extraJar) ||
Files.notExists(extraJarCache) ||
Files.notExists(slimJar) ||
Files.notExists(slimJarCache) ||
Files.notExists(srgJar)) return true;
final String extraJarSha1 = FileUtils.getSHA1(extraJar);
final String slimJarSha1 = FileUtils.getSHA1(slimJar);
String slimJarCacheSha1 = "";
for (final String line : Files.readAllLines(slimJarCache))
{
if(line.contains("Output: "))
{
slimJarCacheSha1 = StringUtils.empty(line, "Output: ");
break;
}
}
String extraJarCacheSha1 = "";
for (final String line : Files.readAllLines(extraJarCache))
{
if(line.contains("Output: "))
{
extraJarCacheSha1 = StringUtils.empty(line, "Output: ");
break;
}
}
return !extraJarSha1.equalsIgnoreCase(extraJarCacheSha1) || !slimJarSha1.equalsIgnoreCase(slimJarCacheSha1);
}
private @NotNull Callable getSha1FromLibrary(@NotNull JsonObject library, String builtJarUrl)
{
final JsonElement checksumsElem = library.get("checksums");
if (checksumsElem != null)
{
final JsonElement checksums = checksumsElem.getAsJsonArray().get(0);
if(checksums != null)
return checksums::getAsString;
}
return () -> IOUtils.getContent(new URL(builtJarUrl + ".sha1"));
}
@Override
public void install(@NotNull Path installDir) throws Exception
{
super.install(installDir);
final String installerUrl = String.format("https://maven.minecraftforge.net/net/minecraftforge/forge/%s/forge-%s-installer.jar",
this.modLoaderVersion, this.modLoaderVersion);
final String[] installerUrlParts = installerUrl.split("/");
final Path installerFile = installDir.resolve(installerUrlParts[installerUrlParts.length - 1]);
IOUtils.download(
this.logger,
new URL(installerUrl),
installerFile
);
if(this.newInstallerJsonSpec)
{
if(this.shouldUseInstaller)
this.useInstaller(installDir, installerFile);
else
{
this.logger.info("Installing libraries...");
final URI uri = URI.create("jar:" + installerFile.toAbsolutePath().toUri());
try (final FileSystem zipFs = FileSystems.newFileSystem(uri, new HashMap<>()))
{
final Path versionFile = zipFs.getPath("version.json");
final Path versionJsonFile = installDir.resolve(this.versionId + ".json");
Files.copy(versionFile, versionJsonFile, StandardCopyOption.REPLACE_EXISTING);
ModLoaderUtils.parseNewVersionInfo(installDir, JsonParser.parseReader(Files.newBufferedReader(versionFile)).getAsJsonObject())
.stream()
.filter(parsedLibrary -> !parsedLibrary.isInstalled())
.forEach(parsedLibrary -> {
if(parsedLibrary.getUrl().isPresent())
parsedLibrary.download(this.logger);
else
{
try
{
final String[] name = parsedLibrary.getArtifact().split(":");
final String group = name[0].replace('.', '/');
final String artifact = name[1];
final boolean hasExtension = name[2].contains("@");
final String version = name[2].contains("@") ? name[2].split("@")[0] : name[2];
final String extension = hasExtension ? name[2].split("@")[1] : "jar";
String classifier = "";
if(name.length == 4)
classifier = "-" + name[3];
Files.createDirectories(parsedLibrary.getPath().getParent());
Files.copy(zipFs.getPath("maven/" + group + '/' + artifact + '/' + version + '/' + artifact + "-" + version + classifier + "." + extension), parsedLibrary.getPath(), StandardCopyOption.REPLACE_EXISTING);
} catch (Exception e)
{
this.logger.printStackTrace(e);
}
}
});
} catch (Exception e)
{
this.logger.printStackTrace(e);
}
}
}
else
{
this.logger.info("Installing libraries...");
final URI uri = URI.create("jar:" + installerFile.toAbsolutePath().toUri());
try (final FileSystem zipFs = FileSystems.newFileSystem(uri, new HashMap<>()))
{
final Path installProfileFile = zipFs.getPath("install_profile.json");
final JsonObject versionInfo = JsonParser.parseReader(Files.newBufferedReader(installProfileFile)).getAsJsonObject().getAsJsonObject("versionInfo");
final Path versionJsonFile = installDir.resolve(this.versionId + ".json");
Files.write(versionJsonFile, versionInfo.toString().getBytes(), StandardOpenOption.CREATE, StandardOpenOption.WRITE);
this.parseOldVersionInfo(installDir, versionInfo)
.stream()
.filter(parsedLibrary -> !parsedLibrary.isInstalled())
.forEach(parsedLibrary -> parsedLibrary.download(this.logger));
} catch (Exception e)
{
this.logger.printStackTrace(e);
}
}
Files.deleteIfExists(installerFile);
}
private void useInstaller(Path installDir, @NotNull Path installerFile) throws Exception
{
this.logger.info("Launching installer...");
ModLoaderUtils.fakeContext(installDir, this.vanilla.getName());
final List command = new ArrayList<>();
command.add(this.javaPath);
command.add("-jar");
command.add(installerFile.toAbsolutePath().toString());
command.add("--installClient");
command.add(installDir.toAbsolutePath().toString());
final ProcessBuilder processBuilder = new ProcessBuilder(command);
processBuilder.directory(installDir.toFile());
processBuilder.redirectOutput(ProcessBuilder.Redirect.INHERIT);
final Process process = processBuilder.start();
process.waitFor();
Files.copy(
installDir.resolve("versions")
.resolve(this.versionId)
.resolve(this.versionId + ".json"),
installDir.resolve(this.versionId + ".json"),
StandardCopyOption.REPLACE_EXISTING
);
ModLoaderUtils.removeFakeContext(installDir);
}
private @NotNull List parseOldVersionInfo(Path installDir, @NotNull JsonObject versionInfo) throws Exception
{
final List parsedLibraries = new ArrayList<>();
final JsonArray libraries = versionInfo.getAsJsonArray("libraries");
for (final JsonElement libraryElement : libraries)
{
final JsonObject library = libraryElement.getAsJsonObject();
final JsonElement clientreqElem = library.get("clientreq");
final boolean shouldInstall = clientreqElem == null || clientreqElem.getAsBoolean();
if(!shouldInstall)
continue;
final JsonElement urlElem = library.get("url");
final String baseUrl = urlElem == null ? "https://libraries.minecraft.net/" : urlElem.getAsString();
final String completeArtifact = library.get("name").getAsString();
final String[] name = completeArtifact.split(":");
final String group = name[0];
final String artifact = name[1];
final String version = name[2];
final String classifier = artifact.equals("forge") ? "-universal" : "";
final Path libraryPath = ModLoaderUtils.buildLibraryPath(installDir, group, artifact, version);
final String builtJarUrl = ModLoaderUtils.buildJarUrl(baseUrl, group, artifact, version, classifier);
final Callable sha1 = this.getSha1FromLibrary(library, builtJarUrl);
final boolean installed = Files.exists(libraryPath) &&
FileUtils.getSHA1(libraryPath).equals(sha1.call());
parsedLibraries.add(new ParsedLibrary(libraryPath, new URL(builtJarUrl), completeArtifact, installed));
}
return parsedLibraries;
}
@Override
public OptiFineInfo getOptiFineInfo()
{
return this.optiFineInfo;
}
@Override
public String name()
{
return "Forge";
}
}
================================================
FILE: src/main/java/fr/flowarg/flowupdater/versions/forge/ForgeVersionBuilder.java
================================================
package fr.flowarg.flowupdater.versions.forge;
import fr.flowarg.flowupdater.download.json.OptiFineInfo;
import fr.flowarg.flowupdater.utils.builderapi.BuilderArgument;
import fr.flowarg.flowupdater.utils.builderapi.BuilderException;
import fr.flowarg.flowupdater.versions.ModLoaderVersionBuilder;
/**
* Builder for {@link ForgeVersion}
* @author Flow Arg (FlowArg)
*/
public class ForgeVersionBuilder extends ModLoaderVersionBuilder
{
private final BuilderArgument forgeVersionArgument = new BuilderArgument("ForgeVersion").required();
private final BuilderArgument optiFineArgument = new BuilderArgument("OptiFine").optional();
/**
* @param forgeVersion the Forge version you want to install. You should be very precise with the string you give.
* For instance, "1.18.2-40.2.21", "1.12.2-14.23.5.2860", "1.8.9-11.15.1.2318-1.8.9", "1.7.10-10.13.4.1614-1.7.10" are correct.
* Download an installer and check the name of it to get the correct version you should provide here if you are not sure.
* @return the builder.
*/
public ForgeVersionBuilder withForgeVersion(String forgeVersion)
{
this.forgeVersionArgument.set(forgeVersion);
return this;
}
/**
* Append some OptiFine download's information.
* @param optiFineInfo provided information.
* @return the builder.
*/
public ForgeVersionBuilder withOptiFine(OptiFineInfo optiFineInfo)
{
this.optiFineArgument.set(optiFineInfo);
return this;
}
@Override
public ForgeVersion build() throws BuilderException
{
return new ForgeVersion(
this.forgeVersionArgument.get(),
this.modsArgument.get(),
this.curseModsArgument.get(),
this.modrinthModsArgument.get(),
this.fileDeleterArgument.get(),
this.curseModPackArgument.get(),
this.modrinthPackArgument.get(),
this.optiFineArgument.get()
);
}
}
================================================
FILE: src/main/java/fr/flowarg/flowupdater/versions/forge/package-info.java
================================================
/**
* This package contains all the classes that are used to install Forge.
*/
package fr.flowarg.flowupdater.versions.forge;
================================================
FILE: src/main/java/fr/flowarg/flowupdater/versions/neoforge/NeoForgeVersion.java
================================================
package fr.flowarg.flowupdater.versions.neoforge;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import fr.flowarg.flowupdater.download.json.*;
import fr.flowarg.flowupdater.utils.IOUtils;
import fr.flowarg.flowupdater.utils.ModFileDeleter;
import fr.flowarg.flowupdater.utils.Version;
import fr.flowarg.flowupdater.versions.AbstractModLoaderVersion;
import fr.flowarg.flowupdater.versions.ModLoaderUtils;
import fr.flowarg.flowupdater.versions.ParsedLibrary;
import org.jetbrains.annotations.NotNull;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.List;
public class NeoForgeVersion extends AbstractModLoaderVersion
{
private final boolean isOldNeoForge; // 1.20.1 neo forge versions only are "old"
private final String versionId;
NeoForgeVersion(String modLoaderVersion, List mods, List curseMods,
List modrinthMods, ModFileDeleter fileDeleter,
CurseModPackInfo curseModPackInfo, ModrinthModPackInfo modrinthModPackInfo, OptiFineInfo optiFineInfo)
{
super(modLoaderVersion, mods, curseMods, modrinthMods, fileDeleter, curseModPackInfo, modrinthModPackInfo, optiFineInfo);
this.isOldNeoForge = this.modLoaderVersion.startsWith("1.");
if(this.isOldNeoForge)
{
final String[] oldNeoForgeVersionData = this.modLoaderVersion.split("-");
final String vanillaVersion = oldNeoForgeVersionData[0];
final String oldNeoForgeVersion = oldNeoForgeVersionData[1];
this.versionId = String.format("%s-forge-%s", vanillaVersion, oldNeoForgeVersion);
}
else this.versionId = String.format("neoforge-%s", this.modLoaderVersion);
}
@Override
public boolean isModLoaderAlreadyInstalled(@NotNull Path installDir)
{
final Path versionJsonFile = installDir.resolve(this.versionId + ".json");
if(Files.notExists(versionJsonFile))
return false;
try {
final JsonObject object = JsonParser.parseReader(Files.newBufferedReader(versionJsonFile))
.getAsJsonObject();
final boolean firstPass = ModLoaderUtils.parseNewVersionInfo(installDir, object).stream().allMatch(ParsedLibrary::isInstalled);
if(!firstPass)
return false;
}
catch (Exception e)
{
this.logger.warn("An error occurred while checking if the mod loader is already installed.");
return false;
}
final Path neoForgeDirectory = installDir.resolve("libraries")
.resolve("net")
.resolve("neoforged")
.resolve(this.isOldNeoForge ? "forge" : "neoforge")
.resolve(this.modLoaderVersion);
final Path universalNeoForgeJar = neoForgeDirectory.resolve(this.versionId + "-universal.jar");
final Path minecraftClientPatchedJar = installDir.resolve("libraries")
.resolve("net")
.resolve("neoforged")
.resolve("minecraft-client-patched")
.resolve(this.modLoaderVersion)
.resolve("minecraft-client-patched-" + this.modLoaderVersion + ".jar"); // starting from 21.10.37-beta
final Path clientNeoForgeJar = neoForgeDirectory.resolve(this.versionId + "-client.jar");
final Version modLoaderVer = Version.gen(this.modLoaderVersion.split("-")[0]); // skip -beta/alpha etc strings
return Files.exists(universalNeoForgeJar) && (
Files.exists(
modLoaderVer.isNewerOrEqualTo(Version.gen("21.10.37")) ? minecraftClientPatchedJar : clientNeoForgeJar
)
);
}
@Override
public void install(@NotNull Path installDir) throws Exception
{
super.install(installDir);
final String installerUrl = String.format(
"https://maven.neoforged.net/releases/net/neoforged/%s/%s/%s-installer.jar",
this.isOldNeoForge ? "forge" : "neoforge",
this.modLoaderVersion,
this.isOldNeoForge ? "forge-" + this.modLoaderVersion : this.versionId
);
ModLoaderUtils.fakeContext(installDir, this.vanilla.getName());
final String[] installerUrlParts = installerUrl.split("/");
final Path installerFile = installDir.resolve(installerUrlParts[installerUrlParts.length - 1]);
IOUtils.download(this.logger, new URL(installerUrl), installerFile);
final List command = new ArrayList<>();
command.add(this.javaPath);
command.add("-jar");
command.add(installerFile.toAbsolutePath().toString());
command.add("--installClient");
command.add(installDir.toAbsolutePath().toString());
final ProcessBuilder processBuilder = new ProcessBuilder(command);
processBuilder.directory(installDir.toFile());
processBuilder.redirectOutput(ProcessBuilder.Redirect.INHERIT);
final Process process = processBuilder.start();
process.waitFor();
Files.copy(
installDir.resolve("versions")
.resolve(this.versionId)
.resolve(this.versionId + ".json"),
installDir.resolve(this.versionId + ".json"),
StandardCopyOption.REPLACE_EXISTING
);
Files.deleteIfExists(installerFile);
ModLoaderUtils.removeFakeContext(installDir);
}
@Override
public String name()
{
return "NeoForge";
}
}
================================================
FILE: src/main/java/fr/flowarg/flowupdater/versions/neoforge/NeoForgeVersionBuilder.java
================================================
package fr.flowarg.flowupdater.versions.neoforge;
import fr.flowarg.flowupdater.download.json.OptiFineInfo;
import fr.flowarg.flowupdater.utils.builderapi.BuilderArgument;
import fr.flowarg.flowupdater.utils.builderapi.BuilderException;
import fr.flowarg.flowupdater.versions.ModLoaderVersionBuilder;
public class NeoForgeVersionBuilder extends ModLoaderVersionBuilder
{
private final BuilderArgument neoForgeVersionArgument = new BuilderArgument("NeoForgeVersion").required();
private final BuilderArgument optiFineArgument = new BuilderArgument("OptiFine").optional();
/**
* @param neoForgeVersion the NeoForge version you want to install.
* For 1.20.1, it should be in the format "1.20.1-47.1.x" (vanilla version-NeoForge version). (forge format)
* For 1.21 and above, it should only be the NeoForge version (for example: 21.8.31) (NeoForge version only).
* @return the builder.
*/
public NeoForgeVersionBuilder withNeoForgeVersion(String neoForgeVersion)
{
this.neoForgeVersionArgument.set(neoForgeVersion);
return this;
}
/**
* Append some OptiFine download's information.
* @param optiFineInfo OptiFine info.
* @return the builder.
*/
public NeoForgeVersionBuilder withOptiFine(OptiFineInfo optiFineInfo)
{
this.optiFineArgument.set(optiFineInfo);
return this;
}
@Override
public NeoForgeVersion build() throws BuilderException
{
return new NeoForgeVersion(
this.neoForgeVersionArgument.get(),
this.modsArgument.get(),
this.curseModsArgument.get(),
this.modrinthModsArgument.get(),
this.fileDeleterArgument.get(),
this.curseModPackArgument.get(),
this.modrinthPackArgument.get(),
this.optiFineArgument.get()
);
}
}
================================================
FILE: src/main/java/fr/flowarg/flowupdater/versions/neoforge/package-info.java
================================================
/**
* This package contains all the classes that are used to install NeoForge.
*/
package fr.flowarg.flowupdater.versions.neoforge;
================================================
FILE: src/main/java/fr/flowarg/flowupdater/versions/package-info.java
================================================
/**
* This package contains all common classes to the versions system.
*/
package fr.flowarg.flowupdater.versions;
================================================
FILE: src/test/java/fr/flowarg/flowupdater/IntegrationTests.java
================================================
package fr.flowarg.flowupdater;
import fr.flowarg.flowio.FileUtils;
import fr.flowarg.flowupdater.utils.UpdaterOptions;
import org.junit.jupiter.api.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class IntegrationTests
{
private static final Path UPDATE_DIR = Paths.get("testing_directory");
@BeforeAll
public static void setup() throws Exception
{
Updates.UPDATE_DIR = UPDATE_DIR;
Files.createDirectory(UPDATE_DIR);
}
@AfterAll
public static void cleanup() throws Exception
{
FileUtils.deleteDirectory(UPDATE_DIR);
}
@Order(1)
@Test
public void testWithVanillaUsage() throws Exception
{
final Updates.Result result = Updates.vanillaUsage();
this.basicAssertions(result.updateDir, result.error, result.version);
}
@Order(2)
@Test
public void testWithNewForgeUsage() throws Exception
{
final Updates.Result result = Updates.testWithNewForgeUsage();
final String vanillaForge = result.version + "-" + result.modLoaderVersion;
this.basicAssertions(result.updateDir, result.error, result.version);
assertTrue(Files.exists(result.updateDir.resolve(String.format("%s-forge-%s.json", result.version, result.modLoaderVersion))));
assertTrue(Files.exists(result.updateDir.resolve("libraries").resolve("net").resolve("minecraftforge").resolve("forge").resolve(vanillaForge).resolve("forge-" + vanillaForge + "-universal.jar")));
}
@Order(3)
@Test
public void testWithVeryOldForgeUsage() throws Exception
{
final Updates.Result result = Updates.testWithVeryOldForgeUsage();
final String full = result.version + '-' + result.modLoaderVersion + '-' + result.version;
this.basicAssertions(result.updateDir, result.error, result.version);
assertTrue(Files.exists(result.updateDir.resolve(result.version + "-Forge" + result.modLoaderVersion + "-" + result.version + ".json")));
assertTrue(Files.exists(result.updateDir.resolve("libraries").resolve("net").resolve("minecraftforge").resolve("forge").resolve(full).resolve("forge-" + full + ".jar")));
}
@Order(4)
@Test
public void testWithOldForgeUsage() throws Exception
{
final Updates.Result result = Updates.testWithOldForgeUsage();
final String full = result.version + '-' + result.modLoaderVersion;
this.basicAssertions(result.updateDir, result.error, result.version);
assertTrue(Files.exists(result.updateDir.resolve(result.version + "-forge" + full + ".json")));
assertTrue(Files.exists(result.updateDir.resolve("libraries").resolve("net").resolve("minecraftforge").resolve("forge").resolve(full).resolve("forge-" + full + ".jar")));
}
@Order(5)
@Test
public void testWithFabric() throws Exception
{
final Updates.Result result = Updates.testWithFabric();
this.basicAssertions(result.updateDir, result.error, result.version);
assertTrue(Files.exists(result.updateDir.resolve("libraries").resolve("net").resolve("fabricmc").resolve("fabric-loader")));
}
@Order(6)
@Test
public void testWithQuilt() throws Exception
{
if(Integer.parseInt(System.getProperty("java.version").split("\\.")[0]) < 17)
{
System.out.println("Skipping test with Quilt because Java version is < 17");
return;
}
final Updates.Result result = Updates.testWithQuilt(new UpdaterOptions.UpdaterOptionsBuilder().build());
this.basicAssertions(result.updateDir, result.error, result.version);
assertTrue(Files.exists(result.updateDir.resolve("libraries").resolve("org").resolve("quiltmc").resolve("quilt-loader")));
}
@Order(6)
@Test
public void testWithFabric119() throws Exception
{
final Updates.Result result = Updates.testWithFabric119();
this.basicAssertions(result.updateDir, result.error, result.version, false);
assertTrue(Files.exists(result.updateDir.resolve("libraries").resolve("net").resolve("fabricmc").resolve("fabric-loader")));
}
@Order(8)
@Test
public void testWithNeoForgeUsage() throws Exception
{
final Updates.Result result = Updates.testWithNeoForgeUsage();
this.basicAssertions(result.updateDir, result.error, result.version, false);
assertTrue(Files.exists(result.updateDir.resolve(String.format("neoforge-%s.json", result.modLoaderVersion))));
assertTrue(Files.exists(result.updateDir.resolve("libraries").resolve("net").resolve("neoforged").resolve("neoforge").resolve(result.modLoaderVersion).resolve("neoforge-" + result.modLoaderVersion + "-universal.jar")));
}
private void basicAssertions(Path updateDir, boolean error, String version) throws Exception
{
this.basicAssertions(updateDir, error, version, true);
}
private void basicAssertions(Path updateDir, boolean error, String version, boolean natives) throws Exception
{
assertFalse(error);
assertTrue(Files.exists(updateDir.resolve(version + ".json")));
assertTrue(Files.exists(updateDir.resolve("client.jar")));
if(natives)
{
final Path nativesDir = updateDir.resolve("natives");
assertTrue(Files.exists(nativesDir));
assertTrue(Files.isDirectory(nativesDir));
assertFalse(FileUtils.list(nativesDir).isEmpty());
}
final Path librariesDir = updateDir.resolve("libraries");
assertTrue(Files.exists(librariesDir));
assertTrue(Files.isDirectory(librariesDir));
assertFalse(FileUtils.list(librariesDir).isEmpty());
FileUtils.list(librariesDir).forEach(path -> assertTrue(Files.isDirectory(path)));
assertTrue(FileUtils.list(updateDir.resolve("assets").resolve("objects")).size() > 200);
}
}
================================================
FILE: src/test/java/fr/flowarg/flowupdater/Updates.java
================================================
package fr.flowarg.flowupdater;
import fr.flowarg.flowupdater.utils.UpdaterOptions;
import fr.flowarg.flowupdater.versions.VanillaVersion;
import fr.flowarg.flowupdater.versions.fabric.FabricVersion;
import fr.flowarg.flowupdater.versions.fabric.FabricVersionBuilder;
import fr.flowarg.flowupdater.versions.fabric.QuiltVersion;
import fr.flowarg.flowupdater.versions.fabric.QuiltVersionBuilder;
import fr.flowarg.flowupdater.versions.forge.ForgeVersion;
import fr.flowarg.flowupdater.versions.forge.ForgeVersionBuilder;
import fr.flowarg.flowupdater.versions.neoforge.NeoForgeVersion;
import fr.flowarg.flowupdater.versions.neoforge.NeoForgeVersionBuilder;
import java.nio.file.Path;
public class Updates
{
public static Path UPDATE_DIR;
public static Result vanillaUsage()
{
final String version = "1.18.2";
final Path updateDir = UPDATE_DIR.resolve("vanilla-" + version);
boolean error = false;
try
{
final VanillaVersion vanillaVersion = new VanillaVersion.VanillaVersionBuilder()
.withName(version)
.build();
final FlowUpdater updater = new FlowUpdater.FlowUpdaterBuilder()
.withVanillaVersion(vanillaVersion)
.build();
updater.update(updateDir);
}
catch (Exception e)
{
error = true;
e.printStackTrace();
}
return new Result(updateDir, error, version);
}
public static Result testWithNewForgeUsage()
{
boolean error = false;
final String vanilla = "1.18.2";
final String forge = "40.2.21";
final String vanillaForge = vanilla + "-" + forge;
final Path updateDir = UPDATE_DIR.resolve("new_forge-" + vanillaForge);
try
{
final VanillaVersion version = new VanillaVersion.VanillaVersionBuilder()
.withName(vanilla)
.build();
final ForgeVersion forgeVersion = new ForgeVersionBuilder()
.withForgeVersion(vanillaForge)
.build();
final FlowUpdater updater = new FlowUpdater.FlowUpdaterBuilder()
.withVanillaVersion(version)
.withModLoaderVersion(forgeVersion)
.build();
updater.update(updateDir);
}
catch (Exception e)
{
error = true;
e.printStackTrace();
}
return new Result(updateDir, error, vanilla, forge);
}
public static Result testWithVeryOldForgeUsage()
{
boolean error = false;
final String vanilla = "1.7.10";
final String forge = "10.13.4.1614";
final String full = vanilla + '-' + forge + '-' + vanilla;
final Path updateDir = UPDATE_DIR.resolve("forge-" + vanilla);
try
{
final VanillaVersion vanillaVersion = new VanillaVersion.VanillaVersionBuilder()
.withName(vanilla)
.build();
final ForgeVersion forgeVersion = new ForgeVersionBuilder()
.withForgeVersion(full)
.build();
final FlowUpdater updater = new FlowUpdater.FlowUpdaterBuilder()
.withVanillaVersion(vanillaVersion)
.withModLoaderVersion(forgeVersion)
.build();
updater.update(updateDir);
}
catch (Exception e)
{
error = true;
e.printStackTrace();
}
return new Result(updateDir, error, vanilla, forge);
}
public static Result testWithOldForgeUsage()
{
boolean error = false;
final String vanilla = "1.8.9";
final String forge = "11.15.1.2318-" + vanilla;
final Path updateDir = UPDATE_DIR.resolve("forge-" + vanilla);
try
{
final VanillaVersion vanillaVersion = new VanillaVersion.VanillaVersionBuilder()
.withName(vanilla)
.build();
final ForgeVersion forgeVersion = new ForgeVersionBuilder()
.withForgeVersion(vanilla + '-' + forge)
.build();
final FlowUpdater updater = new FlowUpdater.FlowUpdaterBuilder()
.withVanillaVersion(vanillaVersion)
.withModLoaderVersion(forgeVersion)
.build();
updater.update(updateDir);
}
catch (Exception e)
{
error = true;
e.printStackTrace();
}
return new Result(updateDir, error, vanilla, forge);
}
public static Result testWithFabric()
{
boolean error = false;
final String version = "1.18.2";
final Path updateDir = UPDATE_DIR.resolve("fabric-" + version);
String fabric = "";
try
{
final VanillaVersion vanillaVersion = new VanillaVersion.VanillaVersionBuilder()
.withName(version)
.build();
final FabricVersion fabricVersion = new FabricVersionBuilder()
.build();
fabric = fabricVersion.getModLoaderVersion();
final FlowUpdater updater = new FlowUpdater.FlowUpdaterBuilder()
.withVanillaVersion(vanillaVersion)
.withModLoaderVersion(fabricVersion)
.build();
updater.update(updateDir);
}
catch (Exception e)
{
error = true;
e.printStackTrace();
}
return new Result(updateDir, error, version, fabric);
}
public static Result testWithQuilt(UpdaterOptions opts)
{
boolean error = false;
String version = "1.18.2";
final Path updateDir = UPDATE_DIR.resolve("quilt-" + version);
String quilt = "";
try
{
final VanillaVersion vanillaVersion = new VanillaVersion.VanillaVersionBuilder()
.withName(version)
.build();
final QuiltVersion quiltVersion = new QuiltVersionBuilder()
.build();
quilt = quiltVersion.getModLoaderVersion();
final FlowUpdater updater = new FlowUpdater.FlowUpdaterBuilder()
.withVanillaVersion(vanillaVersion)
.withModLoaderVersion(quiltVersion)
.withUpdaterOptions(opts)
.build();
updater.update(updateDir);
}
catch (Exception e)
{
error = true;
e.printStackTrace();
}
return new Result(updateDir, error, version, quilt);
}
public static Result testWithFabric119()
{
boolean error = false;
String version = "1.19.4";
final Path updateDir = UPDATE_DIR.resolve("fabric-" + version);
String fabric = "";
try
{
final VanillaVersion vanillaVersion = new VanillaVersion.VanillaVersionBuilder()
.withName(version)
.build();
final FabricVersion fabricVersion = new FabricVersionBuilder()
.build();
fabric = fabricVersion.getModLoaderVersion();
final FlowUpdater updater = new FlowUpdater.FlowUpdaterBuilder()
.withVanillaVersion(vanillaVersion)
.withModLoaderVersion(fabricVersion)
.build();
updater.update(updateDir);
}
catch (Exception e)
{
error = true;
e.printStackTrace();
}
return new Result(updateDir, error, version, fabric);
}
public static Result testWithNeoForgeUsage()
{
boolean error = false;
final String vanilla = "1.20.4";
final String neoForge = "20.4.235";
final Path updateDir = UPDATE_DIR.resolve("neo_forge-" + vanilla);
try
{
final VanillaVersion version = new VanillaVersion.VanillaVersionBuilder()
.withName(vanilla)
.build();
final NeoForgeVersion neoForgeVersion = new NeoForgeVersionBuilder()
.withNeoForgeVersion(neoForge)
.build();
final FlowUpdater updater = new FlowUpdater.FlowUpdaterBuilder()
.withVanillaVersion(version)
.withModLoaderVersion(neoForgeVersion)
.build();
updater.update(updateDir);
}
catch (Exception e)
{
error = true;
e.printStackTrace();
}
return new Result(updateDir, error, vanilla, neoForge);
}
public static Result testWithNeoForgeUsage2()
{
boolean error = false;
final String vanilla = "1.21.1";
final String neoForge = "21.1.18";
final Path updateDir = UPDATE_DIR.resolve("neo_forge-" + vanilla);
try
{
final VanillaVersion version = new VanillaVersion.VanillaVersionBuilder()
.withName(vanilla)
.build();
final NeoForgeVersion neoForgeVersion = new NeoForgeVersionBuilder()
.withNeoForgeVersion(neoForge)
.build();
final FlowUpdater updater = new FlowUpdater.FlowUpdaterBuilder()
.withVanillaVersion(version)
.withModLoaderVersion(neoForgeVersion)
.build();
updater.update(updateDir);
}
catch (Exception e)
{
error = true;
e.printStackTrace();
}
return new Result(updateDir, error, vanilla, neoForge);
}
public static class Result
{
public final Path updateDir;
public final boolean error;
public final String version;
public final String modLoaderVersion;
public Result(Path updateDir, boolean error, String version, String modLoaderVersion)
{
this.updateDir = updateDir;
this.error = error;
this.version = version;
this.modLoaderVersion = modLoaderVersion;
}
public Result(Path updateDir, boolean error, String version)
{
this.updateDir = updateDir;
this.error = error;
this.version = version;
this.modLoaderVersion = null;
}
}
public static Result testWithLast1122Forge()
{
boolean error = false;
final String vanilla = "1.12.2";
final String forge = "14.23.5.2860";
final String vanillaForge = vanilla + "-" + forge;
final Path updateDir = UPDATE_DIR.resolve("last_1122_forge-" + vanillaForge);
try
{
final VanillaVersion version = new VanillaVersion.VanillaVersionBuilder()
.withName(vanilla)
.build();
final ForgeVersion forgeVersion = new ForgeVersionBuilder()
.withForgeVersion(vanillaForge)
.build();
final FlowUpdater updater = new FlowUpdater.FlowUpdaterBuilder()
.withVanillaVersion(version)
.withModLoaderVersion(forgeVersion)
.build();
updater.update(updateDir);
}
catch (Exception e)
{
error = true;
e.printStackTrace();
}
return new Result(updateDir, error, vanilla, forge);
}
public static Result testWith121Forge()
{
boolean error = false;
final String vanilla = "1.21";
final String forge = "51.0.29";
final String vanillaForge = vanilla + "-" + forge;
final Path updateDir = UPDATE_DIR.resolve("121_forge-" + vanillaForge);
try
{
final VanillaVersion version = new VanillaVersion.VanillaVersionBuilder()
.withName(vanilla)
.build();
final ForgeVersion forgeVersion = new ForgeVersionBuilder()
.withForgeVersion(vanillaForge)
.build();
final FlowUpdater updater = new FlowUpdater.FlowUpdaterBuilder()
.withVanillaVersion(version)
.withModLoaderVersion(forgeVersion)
.build();
updater.update(updateDir);
}
catch (Exception e)
{
error = true;
e.printStackTrace();
}
return new Result(updateDir, error, vanilla, forge);
}
}
================================================
FILE: src/test/java/fr/flowarg/flowupdater/utils/VersionTest.java
================================================
package fr.flowarg.flowupdater.utils;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import static org.junit.jupiter.api.Assertions.*;
public class VersionTest
{
@Test
public void testVersionCompareWithSameSize()
{
final Version version = Version.gen("1.0.0");
final Version version2 = Version.gen("1.0.0");
final Version version3 = Version.gen("1.0.1");
final Version version4 = Version.gen("1.1.0");
final Version version5 = Version.gen("2.0.0");
final Version version6 = Version.gen("0.1.0");
final Version version7 = new Version(Arrays.asList(1, 1, 1));
assertEquals(0, version.compareTo(version2));
assertEquals(-1, version.compareTo(version3));
assertEquals(-1, version.compareTo(version4));
assertEquals(-1, version.compareTo(version5));
assertEquals(1, version.compareTo(version6));
assertEquals(-1, version.compareTo(version7));
}
@Test
public void testVersionCompareWithDifferentSize()
{
final Version version = Version.gen("1.0.0");
final Version version2 = Version.gen("1.1");
final Version version3 = Version.gen("1.0");
final Version version4 = Version.gen("3");
final Version version5 = Version.gen("0");
final Version version6 = Version.gen("0.1");
final Version version7 = new Version(Arrays.asList(1, 2, 3, 4, 5));
assertEquals(-1, version.compareTo(version2));
assertEquals(1, version.compareTo(version3));
assertEquals(-1, version.compareTo(version4));
assertEquals(1, version.compareTo(version5));
assertEquals(1, version.compareTo(version6));
assertEquals(-1, version.compareTo(version7));
}
@Test
public void testVersionBetween()
{
final Version version = Version.gen("1.0.0");
final Version version2 = Version.gen("1.1");
final Version version3 = Version.gen("1.0");
final Version version4 = Version.gen("1.0.1");
final Version version5 = Version.gen("1.0.2");
assertTrue(version.isBetweenOrEqual(version3, version2));
assertTrue(version4.isBetweenOrEqual(version, version5));
}
@Test
public void testVersionEmpty()
{
assertThrows(IllegalArgumentException.class, () -> Version.gen(""));
}
}
================================================
FILE: src/test/java/fr/flowarg/flowupdater/utils/builderapi/BuilderAPITest.java
================================================
package fr.flowarg.flowupdater.utils.builderapi;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class BuilderAPITest
{
@Test
public void shouldFailBecauseMissingRequiredArgument()
{
assertThrows(BuilderException.class, () -> new TestBuilder().build());
}
@Test
public void shouldWorkBecauseRequiredArgumentIsFilled()
{
final TestObject object = new TestBuilder().withAnArgument("AnArgument").build();
assertEquals("AnArgument", object.str);
}
@Test
public void shouldFailBecauseOfBadObject()
{
assertThrows(BuilderException.class, () -> new TestBuilder().withAnArgument("AnArgument").withAnInt(-1).build());
}
@Test
public void shouldFailBecauseUndefinedParentArgument()
{
assertThrows(BuilderException.class, () -> new AnotherTestBuilder().withAnotherBoolean(true).build());
}
@Test
public void shouldWorkBecauseDefinedParentArgument()
{
final AnotherTestObject anotherTestObject = new AnotherTestBuilder().withAnotherBoolean(true).withABoolean(false).build();
assertTrue(anotherTestObject.anotherBoolean);
assertFalse(anotherTestObject.aBoolean);
}
private static class TestObject
{
public final String str;
public final int anInt;
public TestObject(String str, int anInt)
{
this.str = str;
this.anInt = anInt;
}
}
private static class AnotherTestObject
{
public final boolean aBoolean;
public final boolean anotherBoolean;
public AnotherTestObject(boolean aBoolean, boolean anotherBoolean)
{
this.aBoolean = aBoolean;
this.anotherBoolean = anotherBoolean;
}
}
private static class TestBuilder implements IBuilder
{
private final BuilderArgument anArgument = new BuilderArgument("AnArgument").required();
private final BuilderArgument anIntArgument = new BuilderArgument<>("AnIntArgument", () -> 0, () -> -1).optional();
public TestBuilder withAnArgument(String anArgument)
{
this.anArgument.set(anArgument);
return this;
}
public TestBuilder withAnInt(int anInt)
{
this.anIntArgument.set(anInt);
return this;
}
@Contract(" -> new")
@Override
public @NotNull TestObject build() throws BuilderException
{
return new TestObject(
this.anArgument.get(),
this.anIntArgument.get()
);
}
}
private static class AnotherTestBuilder implements IBuilder
{
private final BuilderArgument aBooleanArgument = new BuilderArgument("ABooleanArgument").optional();
private final BuilderArgument anotherBooleanArgument = new BuilderArgument("AnotherBooleanArgument").require(this.aBooleanArgument).optional();
public AnotherTestBuilder withABoolean(boolean aBoolean)
{
this.aBooleanArgument.set(aBoolean);
return this;
}
public AnotherTestBuilder withAnotherBoolean(boolean anotherBoolean)
{
this.anotherBooleanArgument.set(anotherBoolean);
return this;
}
@Contract(" -> new")
@Override
public @NotNull AnotherTestObject build() throws BuilderException
{
return new AnotherTestObject(
this.aBooleanArgument.get(),
this.anotherBooleanArgument.get()
);
}
}
}