Compare commits

..

38 Commits

Author SHA1 Message Date
comp500
c0c318772b Improve file check speed, apparently Files.exists is slow 2019-09-04 03:03:29 +01:00
comp500
580408b92a Accept optional mods on the server rather than throwing an exception 2019-08-30 14:52:06 +01:00
comp500
dbdd1fb9f3 Ensure CLI is closed when update is done 2019-08-30 13:57:43 +01:00
comp500
79a983bc2f Don't invalidate files that aren't on the current side 2019-08-30 03:39:15 +01:00
comp500
0cba5ba17b Ensure index and pack files are rechecked after errors 2019-08-12 02:17:28 +01:00
comp500
ce60cdc385 Automagic github release creation 2019-08-12 02:06:49 +01:00
comp500
b314fc8e0b Fix case-sensitivity for standard hashes, add more hash support 2019-08-12 01:37:02 +01:00
comp500
ca4a13589d Fix crash on empty index file 2019-08-11 22:37:42 +01:00
comp500
d21668afa6 ALWAYS IMPLEMENT .hashCode() if its gonna be in a map 2019-08-11 18:08:40 +01:00
comp500
7946377159 Implement optional mods and cancel buttons 2019-08-11 17:49:56 +01:00
comp500
5a54a90f59 Create a wrapper around URIs to fix issues with spaces 2019-08-11 14:36:08 +01:00
comp500
465e4973ba Use ExceptionListWindow to present errors 2019-08-11 01:45:39 +01:00
comp500
ea60175514 Make handleExceptionAndExit better, fix some stuff 2019-08-11 01:14:39 +01:00
comp500
78f5d76fe9 Add showExceptions 2019-08-11 01:01:52 +01:00
comp500
452ab15cc7 Add new GUI code from windowbuilder, fix a thing 2019-08-11 00:53:29 +01:00
comp500
87b00f316a Fix ordering of locationing 2019-08-10 20:13:07 +01:00
comp500
1623c0f880 Correct dialog parentage 2019-08-10 20:08:02 +01:00
comp500
46bbc9b82e Enable text wrapping on the option description 2019-08-10 19:52:19 +01:00
comp500
02b50be782 Ensure files are removed when they need to be 2019-08-10 19:43:41 +01:00
comp500
e6637b9af8 Wrong use of boolean, should be different 2019-08-10 19:37:14 +01:00
comp500
5fc7d6382d Make intellij happy again 2019-08-10 19:31:20 +01:00
comp500
afd8e85754 Change text 2019-08-10 19:30:39 +01:00
comp500
ecbf0b9eba Clean up gitignore mess 2019-08-10 18:46:59 +01:00
comp500
37a1464e11 Make intellij happy 2019-08-10 18:31:12 +01:00
comp500
54fd84a6d8 Add proper options dialog (I do GUI in Windowbuilder usually) 2019-08-10 18:29:40 +01:00
comp500
dcf8d21aad Whoops, I am bad programmer 2019-08-10 02:18:02 +01:00
comp500
eaed3b2187 whoops i java 11'd again 2019-08-10 01:37:39 +01:00
comp500
b22edf920e whoops i java 11'd 2019-08-10 01:26:35 +01:00
comp500
4d8e695fc4 Add cancellation from options list 2019-08-09 20:17:01 +01:00
comp500
a9bd83e96b Implement some stuff, edge case checks, intellij optimisations 2019-08-09 18:15:47 +01:00
comp500
ae085743be Finally, there are no more errors in UpdateManager. Now to actually make it work 2019-08-09 18:03:34 +01:00
comp500
ad79cb3b21 Very fun indeed 2019-08-09 16:31:29 +01:00
comp500
320e56e74e Start rewrite of downloading system, THIS IS SCARY HELP 2019-08-09 15:40:18 +01:00
comp500
bd95bc15ad Thanks IntelliJ. ThintelliJ. 2019-08-07 13:21:15 +01:00
comp500
794b817eff Check for cachedFiles existing 2019-07-04 01:08:24 +01:00
comp500
a5ff63c587 Correctly handle file invalidation 2019-07-03 23:12:11 +01:00
comp500
34a86ffb7d Handle renamed files correctly, log invalidation 2019-07-03 22:49:08 +01:00
comp500
780efe2c9f Remove files correctly, redownload when deleted 2019-07-02 23:32:00 +01:00
33 changed files with 1629 additions and 521 deletions

119
.gitignore vendored
View File

@@ -1,74 +1,87 @@
# Created by https://www.gitignore.io/api/java,gradle,eclipse
# Edit at https://www.gitignore.io/?templates=java,gradle,eclipse
### Eclipse ###
.metadata
bin/
tmp/
*.tmp
*.bak
*.swp
*~.nib
local.properties
.settings/
.loadpath
.recommenders
# Created by https://www.gitignore.io/api/java,gradle,intellij
# Edit at https://www.gitignore.io/?templates=java,gradle,intellij
# External tool builders
.externalToolBuilders/
### Intellij ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# Locally stored "Eclipse launch configurations"
*.launch
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# PyDev specific (Python IDE for Eclipse)
*.pydevproject
# Generated files
.idea/**/contentModel.xml
# CDT-specific (C/C++ Development Tooling)
.cproject
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# CDT- autotools
.autotools
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Java annotation processor (APT)
.factorypath
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# PDT-specific (PHP Development Tools)
.buildpath
# CMake
cmake-build-*/
# sbteclipse plugin
.target
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# Tern plugin
.tern-project
# File-based project format
*.iws
# TeXlipse plugin
.texlipse
# IntelliJ
out/
# STS (Spring Tool Suite)
.springBeans
# mpeltonen/sbt-idea plugin
.idea_modules/
# Code Recommenders
.recommenders/
# JIRA plugin
atlassian-ide-plugin.xml
# Annotation Processing
.apt_generated/
# Cursive Clojure plugin
.idea/replstate.xml
# Scala IDE specific (Scala & Java development for Eclipse)
.cache-main
.scala_dependencies
.worksheet
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
### Eclipse Patch ###
# Eclipse Core
.project
# Editor-based Rest Client
.idea/httpRequests
# JDT-specific (Eclipse Java Development Tools)
.classpath
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
# Annotation Processing
.apt_generated
### Intellij Patch ###
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
.sts4-cache/
# *.iml
# modules.xml
# .idea/misc.xml
# *.ipr
# Sonarlint plugin
.idea/sonarlint
### Java ###
# Compiled class file
@@ -114,4 +127,4 @@ gradle-app.setting
### Gradle Patch ###
**/build/
# End of https://www.gitignore.io/api/java,gradle,eclipse
# End of https://www.gitignore.io/api/java,gradle,intellij

View File

@@ -0,0 +1,36 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="JavaDoc" enabled="true" level="WARNING" enabled_by_default="true">
<option name="TOP_LEVEL_CLASS_OPTIONS">
<value>
<option name="ACCESS_JAVADOC_REQUIRED_FOR" value="none" />
<option name="REQUIRED_TAGS" value="" />
</value>
</option>
<option name="INNER_CLASS_OPTIONS">
<value>
<option name="ACCESS_JAVADOC_REQUIRED_FOR" value="none" />
<option name="REQUIRED_TAGS" value="" />
</value>
</option>
<option name="METHOD_OPTIONS">
<value>
<option name="ACCESS_JAVADOC_REQUIRED_FOR" value="none" />
<option name="REQUIRED_TAGS" value="@return@param@throws or @exception" />
</value>
</option>
<option name="FIELD_OPTIONS">
<value>
<option name="ACCESS_JAVADOC_REQUIRED_FOR" value="none" />
<option name="REQUIRED_TAGS" value="" />
</value>
</option>
<option name="IGNORE_DEPRECATED" value="false" />
<option name="IGNORE_JAVADOC_PERIOD" value="true" />
<option name="IGNORE_DUPLICATED_THROWS" value="false" />
<option name="IGNORE_POINT_TO_ITSELF" value="false" />
<option name="myAdditionalJavadocTags" value="wbp.parser.entryPoint" />
</inspection_tool>
</profile>
</component>

10
.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="EntryPointsManager">
<list size="1">
<item index="0" class="java.lang.String" itemvalue="com.google.gson.annotations.SerializedName" />
</list>
</component>
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="false" project-jdk-name="11" project-jdk-type="JavaSDK" />
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@@ -1,9 +0,0 @@
{
"files.exclude": {
"**/.classpath": true,
"**/.project": true,
"**/.settings": true,
"**/.factorypath": true
},
"java.configuration.updateBuildConfiguration": "interactive"
}

View File

@@ -3,8 +3,11 @@ plugins {
id 'application'
id 'com.github.johnrengelman.shadow' version '5.0.0'
id 'com.palantir.git-version' version '0.11.0'
id 'com.github.breadmoirai.github-release' version '2.2.9'
}
sourceCompatibility = 1.8
dependencies {
implementation 'commons-cli:commons-cli:1.4'
implementation 'com.moandjiezana.toml:toml4j:0.7.2'
@@ -45,4 +48,25 @@ task copyJar(type: Copy) {
into "build/libs/"
}
build.dependsOn copyJar
build.dependsOn copyJar
if (project.hasProperty("github.token")) {
githubRelease {
// IntelliJ u ok?
//noinspection GroovyAssignabilityCheck
owner "comp500"
//noinspection GroovyAssignabilityCheck
repo "packwiz-installer"
//noinspection GroovyAssignabilityCheck
tagName "${project.version}"
//noinspection GroovyAssignabilityCheck
releaseName "Release ${project.version}"
//noinspection GroovyAssignabilityCheck
draft true
//noinspection GroovyAssignabilityCheck
token findProperty("github.token") ?: ""
releaseAssets = [jar.destinationDirectory.file("packwiz-installer.jar").get()]
}
tasks.githubRelease.dependsOn(build)
}

View File

@@ -0,0 +1,245 @@
package link.infra.packwiz.installer;
import link.infra.packwiz.installer.metadata.IndexFile;
import link.infra.packwiz.installer.metadata.ManifestFile;
import link.infra.packwiz.installer.metadata.SpaceSafeURI;
import link.infra.packwiz.installer.metadata.hash.GeneralHashingSource;
import link.infra.packwiz.installer.metadata.hash.Hash;
import link.infra.packwiz.installer.metadata.hash.HashUtils;
import link.infra.packwiz.installer.ui.IExceptionDetails;
import link.infra.packwiz.installer.ui.IOptionDetails;
import okio.Buffer;
import okio.Okio;
import okio.Source;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.List;
class DownloadTask implements IOptionDetails, IExceptionDetails {
final IndexFile.File metadata;
ManifestFile.File cachedFile = null;
private Exception failure = null;
private boolean alreadyUpToDate = false;
private boolean metadataRequired = true;
private boolean invalidated = false;
// If file is new or isOptional changed to true, the option needs to be presented again
private boolean newOptional = true;
private final UpdateManager.Options.Side downloadSide;
private DownloadTask(IndexFile.File metadata, String defaultFormat, UpdateManager.Options.Side downloadSide) {
this.metadata = metadata;
if (metadata.hashFormat == null || metadata.hashFormat.length() == 0) {
metadata.hashFormat = defaultFormat;
}
this.downloadSide = downloadSide;
}
void invalidate() {
invalidated = true;
alreadyUpToDate = false;
}
void updateFromCache(ManifestFile.File cachedFile) {
if (failure != null) return;
if (cachedFile == null) {
this.cachedFile = new ManifestFile.File();
return;
}
this.cachedFile = cachedFile;
if (!invalidated) {
Hash currHash;
try {
currHash = HashUtils.getHash(metadata.hashFormat, metadata.hash);
} catch (Exception e) {
failure = e;
return;
}
if (currHash != null && currHash.equals(cachedFile.hash)) {
// Already up to date
alreadyUpToDate = true;
metadataRequired = false;
}
}
if (cachedFile.isOptional) {
// Because option selection dialog might set this task to true/false, metadata is always needed to download
// the file, and to show the description and name
metadataRequired = true;
}
}
void downloadMetadata(IndexFile parentIndexFile, SpaceSafeURI indexUri) {
if (failure != null) return;
if (metadataRequired) {
try {
metadata.downloadMeta(parentIndexFile, indexUri);
} catch (Exception e) {
failure = e;
return;
}
if (metadata.linkedFile != null) {
if (metadata.linkedFile.option != null) {
if (metadata.linkedFile.option.optional) {
if (cachedFile.isOptional) {
// isOptional didn't change
newOptional = false;
} else {
// isOptional false -> true, set option to it's default value
// TODO: preserve previous option value, somehow??
cachedFile.optionValue = this.metadata.linkedFile.option.defaultValue;
}
}
}
cachedFile.isOptional = isOptional();
cachedFile.onlyOtherSide = !correctSide();
}
}
}
void download(String packFolder, SpaceSafeURI indexUri) {
if (failure != null) return;
// Ensure it is removed
if (!cachedFile.optionValue || !correctSide()) {
if (cachedFile.cachedLocation == null) return;
try {
Files.deleteIfExists(Paths.get(packFolder, cachedFile.cachedLocation));
} catch (IOException e) {
// TODO: how much of a problem is this? use log4j/other log library to show warning?
e.printStackTrace();
}
cachedFile.cachedLocation = null;
return;
}
if (alreadyUpToDate) return;
Path destPath = Paths.get(packFolder, metadata.getDestURI().toString());
// Don't update files marked with preserve if they already exist on disk
if (metadata.preserve) {
if (destPath.toFile().exists()) {
return;
}
}
try {
Hash hash;
String fileHashFormat;
if (metadata.linkedFile != null) {
hash = metadata.linkedFile.getHash();
fileHashFormat = metadata.linkedFile.download.hashFormat;
} else {
hash = metadata.getHash();
fileHashFormat = metadata.hashFormat;
}
Source src = metadata.getSource(indexUri);
GeneralHashingSource fileSource = HashUtils.getHasher(fileHashFormat).getHashingSource(src);
Buffer data = new Buffer();
Okio.buffer(fileSource).readAll(data);
if (fileSource.hashIsEqual(hash)) {
Files.createDirectories(destPath.getParent());
Files.copy(data.inputStream(), destPath, StandardCopyOption.REPLACE_EXISTING);
} else {
// TODO: no more SYSOUT!!!!!!!!!
System.out.println("Invalid hash for " + metadata.getDestURI().toString());
System.out.println("Calculated: " + fileSource.getHash());
System.out.println("Expected: " + hash);
failure = new Exception("Hash invalid!");
}
if (cachedFile.cachedLocation != null && !destPath.equals(Paths.get(packFolder, cachedFile.cachedLocation))) {
// Delete old file if location changes
Files.delete(Paths.get(packFolder, cachedFile.cachedLocation));
}
} catch (Exception e) {
failure = e;
}
if (failure == null) {
if (cachedFile == null) {
cachedFile = new ManifestFile.File();
}
// Update the manifest file
try {
cachedFile.hash = metadata.getHash();
} catch (Exception e) {
failure = e;
return;
}
cachedFile.isOptional = isOptional();
cachedFile.cachedLocation = metadata.getDestURI().toString();
if (metadata.linkedFile != null) {
try {
cachedFile.linkedFileHash = metadata.linkedFile.getHash();
} catch (Exception e) {
failure = e;
}
}
}
}
public Exception getException() {
return failure;
}
boolean isOptional() {
if (metadata.linkedFile != null) {
return metadata.linkedFile.isOptional();
}
return false;
}
boolean isNewOptional() {
return isOptional() && this.newOptional;
}
boolean correctSide() {
if (metadata.linkedFile != null) {
return metadata.linkedFile.side.hasSide(downloadSide);
}
return true;
}
public String getName() {
return metadata.getName();
}
@Override
public boolean getOptionValue() {
return cachedFile.optionValue;
}
@Override
public String getOptionDescription() {
if (metadata.linkedFile != null) {
return metadata.linkedFile.option.description;
}
return null;
}
public void setOptionValue(boolean value) {
if (value && !cachedFile.optionValue) {
// Ensure that an update is done if it changes from false to true, or from true to false
alreadyUpToDate = false;
}
cachedFile.optionValue = value;
}
static List<DownloadTask> createTasksFromIndex(IndexFile index, String defaultFormat, UpdateManager.Options.Side downloadSide) {
ArrayList<DownloadTask> tasks = new ArrayList<>();
for (IndexFile.File file : index.files) {
tasks.add(new DownloadTask(file, defaultFormat, downloadSide));
}
return tasks;
}
}

View File

@@ -1,40 +1,32 @@
package link.infra.packwiz.installer;
import java.awt.EventQueue;
import java.awt.GraphicsEnvironment;
import java.net.URI;
import java.net.URISyntaxException;
import javax.swing.JOptionPane;
import javax.swing.UIManager;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import link.infra.packwiz.installer.metadata.SpaceSafeURI;
import link.infra.packwiz.installer.ui.CLIHandler;
import link.infra.packwiz.installer.ui.IUserInterface;
import link.infra.packwiz.installer.ui.InputStateHandler;
import link.infra.packwiz.installer.ui.InstallWindow;
import org.apache.commons.cli.*;
import javax.swing.*;
import java.awt.*;
import java.net.URISyntaxException;
@SuppressWarnings("unused")
public class Main {
// Actual main() is in RequiresBootstrap!
@SuppressWarnings("unused")
public Main(String[] args) {
// Big overarching try/catch just in case everything breaks
try {
this.startup(args);
} catch (Exception e) {
e.printStackTrace();
EventQueue.invokeLater(new Runnable() {
public void run() {
JOptionPane.showMessageDialog(null,
"A fatal error occurred: \n" + e.getClass().getCanonicalName() + ": " + e.getMessage(),
"packwiz-installer", JOptionPane.ERROR_MESSAGE);
System.exit(1);
}
EventQueue.invokeLater(() -> {
JOptionPane.showMessageDialog(null,
"A fatal error occurred: \n" + e.getClass().getCanonicalName() + ": " + e.getMessage(),
"packwiz-installer", JOptionPane.ERROR_MESSAGE);
System.exit(1);
});
// In case the eventqueue is broken, exit after 1 minute
try {
@@ -47,7 +39,7 @@ public class Main {
}
}
protected void startup(String[] args) {
private void startup(String[] args) {
Options options = new Options();
addNonBootstrapOptions(options);
addBootstrapOptions(options);
@@ -89,7 +81,8 @@ public class Main {
ui.setTitle(title);
}
ui.show();
InputStateHandler inputStateHandler = new InputStateHandler();
ui.show(inputStateHandler);
UpdateManager.Options uOptions = new UpdateManager.Options();
@@ -109,7 +102,7 @@ public class Main {
}
try {
uOptions.downloadURI = new URI(unparsedArgs[0]);
uOptions.downloadURI = new SpaceSafeURI(unparsedArgs[0]);
} catch (URISyntaxException e) {
// TODO: better error message?
ui.handleExceptionAndExit(e);
@@ -119,26 +112,22 @@ public class Main {
// Start update process!
// TODO: start in SwingWorker?
try {
ui.executeManager(new Runnable(){
@Override
public void run() {
try {
new UpdateManager(uOptions, ui);
} catch (Exception e) {
// TODO: better error message?
ui.handleExceptionAndExit(e);
return;
}
ui.executeManager(() -> {
try {
new UpdateManager(uOptions, ui, inputStateHandler);
} catch (Exception e) {
// TODO: better error message?
ui.handleExceptionAndExit(e);
}
});
} catch (Exception e) {
// TODO: better error message?
ui.handleExceptionAndExit(e);
return;
}
}
// Called by packwiz-installer-bootstrap to set up the help command
@SuppressWarnings("WeakerAccess")
public static void addNonBootstrapOptions(Options options) {
options.addOption("s", "side", true, "Side to install mods from (client/server, defaults to client)");
options.addOption(null, "title", true, "Title of the installer window");

View File

@@ -1,10 +1,8 @@
package link.infra.packwiz.installer;
import javax.swing.*;
import java.util.Arrays;
import javax.swing.JOptionPane;
import javax.swing.UIManager;
public class RequiresBootstrap {
public static void main(String[] args) {
@@ -15,10 +13,10 @@ public class RequiresBootstrap {
if (Arrays.stream(args).map(str -> {
if (str == null) return "";
if (str.startsWith("--")) {
return str.substring(2, str.length());
return str.substring(2);
}
if (str.startsWith("-")) {
return str.substring(1, str.length());
return str.substring(1);
}
return "";
}).anyMatch(str -> str.equals("g") || str.equals("no-gui"))) {

View File

@@ -1,60 +1,54 @@
package link.infra.packwiz.installer;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletionService;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonIOException;
import com.google.gson.JsonSyntaxException;
import com.google.gson.annotations.SerializedName;
import com.moandjiezana.toml.Toml;
import link.infra.packwiz.installer.metadata.IndexFile;
import link.infra.packwiz.installer.metadata.ManifestFile;
import link.infra.packwiz.installer.metadata.PackFile;
import link.infra.packwiz.installer.metadata.SpaceSafeURI;
import link.infra.packwiz.installer.metadata.hash.GeneralHashingSource;
import link.infra.packwiz.installer.metadata.hash.Hash;
import link.infra.packwiz.installer.metadata.hash.HashUtils;
import link.infra.packwiz.installer.request.HandlerManager;
import link.infra.packwiz.installer.ui.IExceptionDetails;
import link.infra.packwiz.installer.ui.IUserInterface;
import link.infra.packwiz.installer.ui.InputStateHandler;
import link.infra.packwiz.installer.ui.InstallProgress;
import okio.Buffer;
import okio.Okio;
import okio.Source;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.*;
import java.util.stream.Collectors;
public class UpdateManager {
public final Options opts;
private final Options opts;
public final IUserInterface ui;
private boolean cancelled;
private boolean cancelledStartGame = false;
private InputStateHandler stateHandler;
private boolean errorsOccurred = false;
public static class Options {
public URI downloadURI = null;
public String manifestFile = "packwiz.json"; // TODO: make configurable
public String packFolder = ".";
public Side side = Side.CLIENT;
SpaceSafeURI downloadURI = null;
String manifestFile = "packwiz.json"; // TODO: make configurable
String packFolder = ".";
Side side = Side.CLIENT;
public static enum Side {
public enum Side {
@SerializedName("client")
CLIENT("client"), @SerializedName("server")
SERVER("server"), @SerializedName("both")
CLIENT("client"),
@SerializedName("server")
SERVER("server"),
@SerializedName("both")
BOTH("both", new Side[] { CLIENT, SERVER });
private final String sideName;
@@ -80,8 +74,8 @@ public class UpdateManager {
return true;
}
if (this.depSides != null) {
for (int i = 0; i < this.depSides.length; i++) {
if (this.depSides[i].equals(tSide)) {
for (Side depSide : this.depSides) {
if (depSide.equals(tSide)) {
return true;
}
}
@@ -92,7 +86,7 @@ public class UpdateManager {
public static Side from(String name) {
String lowerName = name.toLowerCase();
for (Side side : Side.values()) {
if (side.sideName == lowerName) {
if (side.sideName.equals(lowerName)) {
return side;
}
}
@@ -101,13 +95,14 @@ public class UpdateManager {
}
}
public UpdateManager(Options opts, IUserInterface ui) {
UpdateManager(Options opts, IUserInterface ui, InputStateHandler inputStateHandler) {
this.opts = opts;
this.ui = ui;
this.stateHandler = inputStateHandler;
this.start();
}
protected void start() {
private void start() {
this.checkOptions();
ui.submitProgress(new InstallProgress("Loading manifest file..."));
@@ -123,6 +118,11 @@ public class UpdateManager {
return;
}
if (stateHandler.getCancelButton()) {
showCancellationDialog();
handleCancellation();
}
ui.submitProgress(new InstallProgress("Loading pack file..."));
GeneralHashingSource packFileSource;
try {
@@ -142,24 +142,77 @@ public class UpdateManager {
return;
}
if (manifest.packFileHash != null && packFileSource.hashIsEqual(manifest.packFileHash)) {
if (stateHandler.getCancelButton()) {
showCancellationDialog();
handleCancellation();
}
ui.submitProgress(new InstallProgress("Checking local files..."));
// Invalidation checking must be done here, as it must happen before pack/index hashes are checked
List<SpaceSafeURI> invalidatedUris = new ArrayList<>();
if (manifest.cachedFiles != null) {
for (Map.Entry<SpaceSafeURI, ManifestFile.File> entry : manifest.cachedFiles.entrySet()) {
// ignore onlyOtherSide files
if (entry.getValue().onlyOtherSide) {
continue;
}
boolean invalid = false;
// if isn't optional, or is optional but optionValue == true
if (!entry.getValue().isOptional || entry.getValue().optionValue) {
if (entry.getValue().cachedLocation != null) {
if (!Paths.get(opts.packFolder, entry.getValue().cachedLocation).toFile().exists()) {
invalid = true;
}
} else {
// if cachedLocation == null, should probably be installed!!
invalid = true;
}
}
if (invalid) {
SpaceSafeURI fileUri = entry.getKey();
System.out.println("File " + fileUri.toString() + " invalidated, marked for redownloading");
invalidatedUris.add(fileUri);
}
}
}
if (manifest.packFileHash != null && packFileSource.hashIsEqual(manifest.packFileHash) && invalidatedUris.isEmpty()) {
System.out.println("Modpack is already up to date!");
// todo: --force?
return;
if (!stateHandler.getOptionsButton()) {
return;
}
}
System.out.println("Modpack name: " + pf.name);
if (stateHandler.getCancelButton()) {
showCancellationDialog();
handleCancellation();
}
try {
// This is badly written, I'll probably heavily refactor it at some point
processIndex(HandlerManager.getNewLoc(opts.downloadURI, pf.index.file),
HashUtils.getHash(pf.index.hashFormat, pf.index.hash), pf.index.hashFormat, manifest);
HashUtils.getHash(pf.index.hashFormat, pf.index.hash), pf.index.hashFormat, manifest, invalidatedUris);
} catch (Exception e1) {
ui.handleExceptionAndExit(e1);
}
handleCancellation();
// TODO: update MMC params, java args etc
manifest.packFileHash = packFileSource.getHash();
// If there were errors, don't write the manifest/index hashes, to ensure they are rechecked later
if (errorsOccurred) {
manifest.indexFileHash = null;
manifest.packFileHash = null;
} else {
manifest.packFileHash = packFileSource.getHash();
}
manifest.cachedSide = opts.side;
try (Writer writer = new FileWriter(Paths.get(opts.packFolder, opts.manifestFile).toString())) {
gson.toJson(manifest, writer);
} catch (IOException e) {
@@ -169,14 +222,16 @@ public class UpdateManager {
}
protected void checkOptions() {
private void checkOptions() {
// TODO: implement
}
protected void processIndex(URI indexUri, Hash indexHash, String hashFormat, ManifestFile manifest) {
if (manifest.indexFileHash != null && manifest.indexFileHash.equals(indexHash)) {
private void processIndex(SpaceSafeURI indexUri, Hash indexHash, String hashFormat, ManifestFile manifest, List<SpaceSafeURI> invalidatedUris) {
if (manifest.indexFileHash != null && manifest.indexFileHash.equals(indexHash) && invalidatedUris.isEmpty()) {
System.out.println("Modpack files are already up to date!");
return;
if (!stateHandler.getOptionsButton()) {
return;
}
}
manifest.indexFileHash = indexHash;
@@ -200,177 +255,246 @@ public class UpdateManager {
if (!indexFileSource.hashIsEqual(indexHash)) {
// TODO: throw exception
System.out.println("I was meant to put an error message here but I'll do that later");
return;
}
if (stateHandler.getCancelButton()) {
showCancellationDialog();
return;
}
if (manifest.cachedFiles == null) {
manifest.cachedFiles = new HashMap<URI, ManifestFile.File>();
manifest.cachedFiles = new HashMap<>();
}
// TODO: progress bar
ConcurrentLinkedQueue<Exception> exceptionQueue = new ConcurrentLinkedQueue<Exception>();
List<IndexFile.File> newFiles = indexFile.files.stream().map(f -> {
if (f.hashFormat == null || f.hashFormat.length() == 0) {
f.hashFormat = indexFile.hashFormat;
ui.submitProgress(new InstallProgress("Checking local files..."));
Iterator<Map.Entry<SpaceSafeURI, ManifestFile.File>> it = manifest.cachedFiles.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<SpaceSafeURI, ManifestFile.File> entry = it.next();
if (entry.getValue().cachedLocation != null) {
boolean alreadyDeleted = false;
// Delete if option value has been set to false
if (entry.getValue().isOptional && !entry.getValue().optionValue) {
try {
Files.deleteIfExists(Paths.get(opts.packFolder, entry.getValue().cachedLocation));
} catch (IOException e) {
// TODO: should this be shown to the user in some way?
e.printStackTrace();
}
// Set to null, as it doesn't exist anymore
entry.getValue().cachedLocation = null;
alreadyDeleted = true;
}
if (indexFile.files.stream().noneMatch(f -> f.file.equals(entry.getKey()))) {
// File has been removed from the index
if (!alreadyDeleted) {
try {
Files.deleteIfExists(Paths.get(opts.packFolder, entry.getValue().cachedLocation));
} catch (IOException e) {
// TODO: should this be shown to the user in some way?
e.printStackTrace();
}
}
it.remove();
}
}
return f;
}).filter(f -> {
ManifestFile.File cachedFile = manifest.cachedFiles.get(f.file);
Hash newHash;
try {
newHash = HashUtils.getHash(f.hashFormat, f.hash);
} catch (Exception e) {
exceptionQueue.add(e);
return false;
}
return cachedFile == null || !newHash.equals(cachedFile.hash);
}).parallel().map(f -> {
try {
f.downloadMeta(indexFile, indexUri);
} catch (Exception e) {
exceptionQueue.add(e);
}
return f;
}).collect(Collectors.toList());
for (Exception e : exceptionQueue) {
// TODO: collect all exceptions, present in one dialog
ui.handleException(e);
}
// TODO: present options
// TODO: all options should be presented, not just new files!!!!!!!
// and options should be readded to newFiles after option -> true
newFiles.stream().filter(f -> f.linkedFile != null).filter(f -> f.linkedFile.option != null).map(f -> {
return "option: " + (f.linkedFile.option.description == null ? "null" : f.linkedFile.option.description);
}).forEachOrdered(desc -> {
System.out.println(desc);
if (stateHandler.getCancelButton()) {
showCancellationDialog();
return;
}
ui.submitProgress(new InstallProgress("Comparing new files..."));
// TODO: progress bar?
if (indexFile.files == null || indexFile.files.size() == 0) {
System.out.println("Warning: Index is empty!");
indexFile.files = new ArrayList<>();
}
List<DownloadTask> tasks = DownloadTask.createTasksFromIndex(indexFile, indexFile.hashFormat, opts.side);
// If the side changes, invalidate EVERYTHING just in case
// Might not be needed, but done just to be safe
boolean invalidateAll = !opts.side.equals(manifest.cachedSide);
if (invalidateAll) {
System.out.println("Side changed, invalidating all mods");
}
tasks.forEach(f -> {
// TODO: should linkedfile be checked as well? should this be done in the download section?
if (invalidateAll) {
f.invalidate();
} else if (invalidatedUris.contains(f.metadata.file)) {
f.invalidate();
}
ManifestFile.File file = manifest.cachedFiles.get(f.metadata.file);
if (file != null) {
// Ensure the file can be reverted later if necessary - the DownloadTask modifies the file so if it fails we need the old version back
file.backup();
}
// If it is null, the DownloadTask will make a new empty cachedFile
f.updateFromCache(file);
});
if (stateHandler.getCancelButton()) {
showCancellationDialog();
return;
}
// Let's hope downloadMetadata is a pure function!!!
tasks.parallelStream().forEach(f -> f.downloadMetadata(indexFile, indexUri));
List<IExceptionDetails> failedTasks = tasks.stream().filter(t -> t.getException() != null).collect(Collectors.toList());
if (!failedTasks.isEmpty()) {
errorsOccurred = true;
IExceptionDetails.ExceptionListResult exceptionListResult;
try {
exceptionListResult = ui.showExceptions(failedTasks, tasks.size(), true).get();
} catch (InterruptedException | ExecutionException e) {
// Interrupted means cancelled???
ui.handleExceptionAndExit(e);
return;
}
switch (exceptionListResult) {
case CONTINUE:
break;
case CANCEL:
cancelled = true;
return;
case IGNORE:
cancelledStartGame = true;
return;
}
}
if (stateHandler.getCancelButton()) {
showCancellationDialog();
return;
}
List<DownloadTask> nonFailedFirstTasks = tasks.stream().filter(t -> t.getException() == null).collect(Collectors.toList());
List<DownloadTask> optionTasks = nonFailedFirstTasks.stream().filter(DownloadTask::correctSide).filter(DownloadTask::isOptional).collect(Collectors.toList());
// If options changed, present all options again
if (stateHandler.getOptionsButton() || optionTasks.stream().anyMatch(DownloadTask::isNewOptional)) {
// new ArrayList is requires so it's an IOptionDetails rather than a DownloadTask list
Future<Boolean> cancelledResult = ui.showOptions(new ArrayList<>(optionTasks));
try {
if (cancelledResult.get()) {
cancelled = true;
// TODO: Should the UI be closed somehow??
return;
}
} catch (InterruptedException | ExecutionException e) {
// Interrupted means cancelled???
ui.handleExceptionAndExit(e);
}
}
ui.disableOptionsButton();
// TODO: different thread pool type?
ExecutorService threadPool = Executors.newFixedThreadPool(10);
CompletionService<DownloadCompletion> completionService = new ExecutorCompletionService<DownloadCompletion>(
threadPool);
CompletionService<DownloadTask> completionService = new ExecutorCompletionService<>(threadPool);
for (IndexFile.File f : newFiles) {
ManifestFile.File cachedFile = manifest.cachedFiles.get(f.file);
completionService.submit(new Callable<DownloadCompletion>() {
public DownloadCompletion call() {
DownloadCompletion dc = new DownloadCompletion();
dc.file = f;
tasks.forEach(t -> completionService.submit(() -> {
t.download(opts.packFolder, indexUri);
return t;
}));
if (cachedFile != null && cachedFile.linkedFileHash != null && f.linkedFile != null) {
try {
if (cachedFile.linkedFileHash.equals(f.linkedFile.getHash())) {
// Do nothing, the file didn't change
// TODO: but if the hash of the metafile changed, what did change?????
// should this be checked somehow??
return dc;
}
} catch (Exception e) {}
}
// Don't update files marked with preserve if they already exist on disk
if (f.preserve) {
if (Files.exists(Paths.get(opts.packFolder, f.getDestURI().toString()))) {
return dc;
}
}
try {
Hash hash;
String fileHashFormat;
if (f.linkedFile != null) {
hash = f.linkedFile.getHash();
fileHashFormat = f.linkedFile.download.hashFormat;
} else {
hash = f.getHash();
fileHashFormat = f.hashFormat;
}
Source src = f.getSource(indexUri);
GeneralHashingSource fileSource = HashUtils.getHasher(fileHashFormat).getHashingSource(src);
Buffer data = new Buffer();
Okio.buffer(fileSource).readAll(data);
if (fileSource.hashIsEqual(hash)) {
Files.createDirectories(Paths.get(opts.packFolder, f.getDestURI().toString()).getParent());
Files.copy(data.inputStream(), Paths.get(opts.packFolder, f.getDestURI().toString()), StandardCopyOption.REPLACE_EXISTING);
} else {
System.out.println("Invalid hash for " + f.getDestURI().toString());
System.out.println("Calculated: " + fileSource.getHash());
System.out.println("Expected: " + hash);
dc.err = new Exception("Hash invalid!");
}
return dc;
} catch (Exception e) {
dc.err = e;
return dc;
}
}
});
}
for (int i = 0; i < newFiles.size(); i++) {
DownloadCompletion ret;
for (int i = 0; i < tasks.size(); i++) {
DownloadTask task;
try {
ret = completionService.take().get();
task = completionService.take().get();
} catch (InterruptedException | ExecutionException e) {
// TODO: collect all exceptions, present in one dialog
ui.handleException(e);
ret = null;
task = null;
}
// Update manifest
if (ret != null && ret.err == null && ret.file != null) {
ManifestFile.File newCachedFile = new ManifestFile.File();
try {
newCachedFile.hash = ret.file.getHash();
if (newCachedFile.hash == null) {
throw new Exception("Invalid hash!");
// Update manifest - If there were no errors cachedFile has already been modified in place (good old pass by reference)
if (task != null) {
if (task.getException() != null) {
ManifestFile.File file = task.cachedFile.getRevert();
if (file != null) {
manifest.cachedFiles.putIfAbsent(task.metadata.file, file);
}
} catch (Exception e) {
ret.err = e;
}
if (ret.file.metafile && ret.file.linkedFile != null) {
newCachedFile.isOptional = ret.file.linkedFile.isOptional();
if (newCachedFile.isOptional) {
newCachedFile.optionValue = ret.file.optionValue;
}
try {
newCachedFile.linkedFileHash = ret.file.linkedFile.getHash();
} catch (Exception e) {
ret.err = e;
}
}
manifest.cachedFiles.put(ret.file.file, newCachedFile);
}
// TODO: show errors properly?
String progress;
if (ret != null) {
if (ret.err != null) {
if (ret.file != null) {
progress = "Failed to download " + ret.file.getName() + ": " + ret.err.getMessage();
} else {
progress = "Failed to download: " + ret.err.getMessage();
}
ret.err.printStackTrace();
} else if (ret.file != null) {
progress = "Downloaded " + ret.file.getName();
} else {
progress = "Failed to download, unknown reason";
// idiot, if it wasn't there in the first place it won't magically appear there
manifest.cachedFiles.putIfAbsent(task.metadata.file, task.cachedFile);
}
}
String progress;
if (task != null) {
if (task.getException() != null) {
progress = "Failed to download " + task.metadata.getName() + ": " + task.getException().getMessage();
task.getException().printStackTrace();
} else {
// TODO: should this be revised for tasks that didn't actually download it?
progress = "Downloaded " + task.metadata.getName();
}
} else {
progress = "Failed to download, unknown reason";
}
ui.submitProgress(new InstallProgress(progress, i + 1, newFiles.size()));
ui.submitProgress(new InstallProgress(progress, i + 1, tasks.size()));
if (stateHandler.getCancelButton()) {
// Stop all tasks, don't launch the game (it's in an invalid state!)
threadPool.shutdown();
cancelled = true;
return;
}
}
// Shut down the thread pool when the update is done
threadPool.shutdown();
List<IExceptionDetails> failedTasks2ElectricBoogaloo = nonFailedFirstTasks.stream().filter(t -> t.getException() != null).collect(Collectors.toList());
if (!failedTasks2ElectricBoogaloo.isEmpty()) {
errorsOccurred = true;
IExceptionDetails.ExceptionListResult exceptionListResult;
try {
exceptionListResult = ui.showExceptions(failedTasks2ElectricBoogaloo, tasks.size(), false).get();
} catch (InterruptedException | ExecutionException e) {
// Interrupted means cancelled???
ui.handleExceptionAndExit(e);
return;
}
switch (exceptionListResult) {
case CONTINUE:
break;
case CANCEL:
cancelled = true;
return;
case IGNORE:
cancelledStartGame = true;
}
}
// option = false file hashes should be stored to disk, but not downloaded
// TODO: don't include optional files in progress????
}
private class DownloadCompletion {
Exception err;
IndexFile.File file;
private void showCancellationDialog() {
IExceptionDetails.ExceptionListResult exceptionListResult;
try {
exceptionListResult = ui.showCancellationDialog().get();
} catch (InterruptedException | ExecutionException e) {
// Interrupted means cancelled???
ui.handleExceptionAndExit(e);
return;
}
switch (exceptionListResult) {
case CONTINUE:
throw new RuntimeException("Continuation not allowed here!");
case CANCEL:
cancelled = true;
return;
case IGNORE:
cancelledStartGame = true;
}
}
private void handleCancellation() {
if (cancelled) {
System.out.println("Update cancelled by user!");
System.exit(1);
} else if (cancelledStartGame) {
System.out.println("Update cancelled by user! Continuing to start game...");
System.exit(0);
}
}
}

View File

@@ -0,0 +1,29 @@
package link.infra.packwiz.installer.metadata;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
public class EfficientBooleanAdapter extends TypeAdapter<Boolean> {
@Override
public void write(JsonWriter out, Boolean value) throws IOException {
if (value == null || !value) {
out.nullValue();
return;
}
out.value(true);
}
@Override
public Boolean read(JsonReader in) throws IOException {
if (in.peek() == JsonToken.NULL) {
in.nextNull();
return false;
}
return in.nextBoolean();
}
}

View File

@@ -1,13 +1,7 @@
package link.infra.packwiz.installer.metadata;
import java.net.URI;
import java.nio.file.Paths;
import java.util.List;
import com.google.gson.annotations.JsonAdapter;
import com.google.gson.annotations.SerializedName;
import com.moandjiezana.toml.Toml;
import link.infra.packwiz.installer.metadata.hash.GeneralHashingSource;
import link.infra.packwiz.installer.metadata.hash.Hash;
import link.infra.packwiz.installer.metadata.hash.HashUtils;
@@ -15,26 +9,27 @@ import link.infra.packwiz.installer.request.HandlerManager;
import okio.Okio;
import okio.Source;
import java.nio.file.Paths;
import java.util.List;
public class IndexFile {
@SerializedName("hash-format")
public String hashFormat;
public List<File> files;
public static class File {
@JsonAdapter(SpaceSafeURIParser.class)
public URI file;
public SpaceSafeURI file;
@SerializedName("hash-format")
public String hashFormat;
public String hash;
public URI alias;
public SpaceSafeURI alias;
public boolean metafile;
public boolean preserve;
public transient ModFile linkedFile;
public transient URI linkedFileURI;
public transient boolean optionValue = true;
public transient SpaceSafeURI linkedFileURI;
public void downloadMeta(IndexFile parentIndexFile, URI indexUri) throws Exception {
public void downloadMeta(IndexFile parentIndexFile, SpaceSafeURI indexUri) throws Exception {
if (!metafile) {
return;
}
@@ -52,14 +47,14 @@ public class IndexFile {
}
}
public Source getSource(URI indexUri) throws Exception {
public Source getSource(SpaceSafeURI indexUri) throws Exception {
if (metafile) {
if (linkedFile == null) {
throw new Exception("Linked file doesn't exist!");
}
return linkedFile.getSource(linkedFileURI);
} else {
URI newLoc = HandlerManager.getNewLoc(indexUri, file);
SpaceSafeURI newLoc = HandlerManager.getNewLoc(indexUri, file);
if (newLoc == null) {
throw new Exception("Index file URI is invalid");
}
@@ -69,6 +64,7 @@ public class IndexFile {
public Hash getHash() throws Exception {
if (hash == null) {
// TODO: should these be more specific exceptions (e.g. IndexFileException?!)
throw new Exception("Index file doesn't have a hash");
}
if (hashFormat == null) {
@@ -90,16 +86,17 @@ public class IndexFile {
if (file != null) {
return Paths.get(file.getPath()).getFileName().toString();
}
return file.getPath();
// TODO: throw some kind of exception?
return "Invalid file";
}
public URI getDestURI() {
public SpaceSafeURI getDestURI() {
if (alias != null) {
return alias;
}
if (metafile && linkedFile != null) {
// TODO: URIs are bad
return file.resolve(linkedFile.filename.replace(" ", "%20"));
return file.resolve(linkedFile.filename);
} else {
return file;
}

View File

@@ -1,20 +1,45 @@
package link.infra.packwiz.installer.metadata;
import java.net.URI;
import java.util.Map;
import com.google.gson.annotations.JsonAdapter;
import link.infra.packwiz.installer.UpdateManager;
import link.infra.packwiz.installer.metadata.hash.Hash;
import java.util.Map;
public class ManifestFile {
public Hash packFileHash = null;
public Hash indexFileHash = null;
public Map<URI, File> cachedFiles;
public Map<SpaceSafeURI, File> cachedFiles;
// If the side changes, EVERYTHING invalidates. FUN!!!
public UpdateManager.Options.Side cachedSide = UpdateManager.Options.Side.CLIENT;
public static class File {
private transient File revert;
public Hash hash = null;
public Hash linkedFileHash = null;
public String cachedLocation = null;
@JsonAdapter(EfficientBooleanAdapter.class)
public boolean isOptional = false;
public boolean optionValue = true;
public Hash linkedFileHash = null;
@JsonAdapter(EfficientBooleanAdapter.class)
public boolean onlyOtherSide = false;
// When an error occurs, the state needs to be reverted. To do this, I have a crude revert system.
public void backup() {
revert = new File();
revert.hash = hash;
revert.linkedFileHash = linkedFileHash;
revert.cachedLocation = cachedLocation;
revert.isOptional = isOptional;
revert.optionValue = optionValue;
revert.onlyOtherSide = onlyOtherSide;
}
public File getRevert() {
return revert;
}
}
}

View File

@@ -1,17 +1,14 @@
package link.infra.packwiz.installer.metadata;
import java.net.URI;
import java.util.Map;
import com.google.gson.annotations.JsonAdapter;
import com.google.gson.annotations.SerializedName;
import link.infra.packwiz.installer.UpdateManager.Options.Side;
import link.infra.packwiz.installer.metadata.hash.Hash;
import link.infra.packwiz.installer.metadata.hash.HashUtils;
import link.infra.packwiz.installer.request.HandlerManager;
import okio.Source;
import java.util.Map;
public class ModFile {
public String name;
public String filename;
@@ -19,8 +16,7 @@ public class ModFile {
public Download download;
public static class Download {
@JsonAdapter(SpaceSafeURIParser.class)
public URI url;
public SpaceSafeURI url;
@SerializedName("hash-format")
public String hashFormat;
public String hash;
@@ -36,14 +32,14 @@ public class ModFile {
public boolean defaultValue;
}
public Source getSource(URI baseLoc) throws Exception {
public Source getSource(SpaceSafeURI baseLoc) throws Exception {
if (download == null) {
throw new Exception("Metadata file doesn't have download");
}
if (download.url == null) {
throw new Exception("Metadata file doesn't have a download URI");
}
URI newLoc = HandlerManager.getNewLoc(baseLoc, download.url);
SpaceSafeURI newLoc = HandlerManager.getNewLoc(baseLoc, download.url);
if (newLoc == null) {
throw new Exception("Metadata file URI is invalid");
}

View File

@@ -1,18 +1,15 @@
package link.infra.packwiz.installer.metadata;
import java.net.URI;
import java.util.Map;
import com.google.gson.annotations.JsonAdapter;
import com.google.gson.annotations.SerializedName;
import java.util.Map;
public class PackFile {
public String name;
public IndexFileLoc index;
public static class IndexFileLoc {
@JsonAdapter(SpaceSafeURIParser.class)
public URI file;
public SpaceSafeURI file;
@SerializedName("hash-format")
public String hashFormat;
public String hash;

View File

@@ -0,0 +1,88 @@
package link.infra.packwiz.installer.metadata;
import com.google.gson.annotations.JsonAdapter;
import java.io.Serializable;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
// The world's worst URI wrapper
@JsonAdapter(SpaceSafeURIParser.class)
public class SpaceSafeURI implements Comparable<SpaceSafeURI>, Serializable {
private final URI u;
public SpaceSafeURI(String str) throws URISyntaxException {
u = new URI(str.replace(" ", "%20"));
}
public SpaceSafeURI(URI uri) {
this.u = uri;
}
public SpaceSafeURI(String scheme, String authority, String path, String query, String fragment) throws URISyntaxException {
// TODO: do all components need to be replaced?
scheme = scheme.replace(" ", "%20");
authority = authority.replace(" ", "%20");
path = path.replace(" ", "%20");
query = query.replace(" ", "%20");
fragment = fragment.replace(" ", "%20");
u = new URI(scheme, authority, path, query, fragment);
}
public String getPath() {
return u.getPath().replace("%20", " ");
}
public String toString() {
return u.toString().replace("%20", " ");
}
@SuppressWarnings("WeakerAccess")
public SpaceSafeURI resolve(String path) {
return new SpaceSafeURI(u.resolve(path.replace(" ", "%20")));
}
public SpaceSafeURI resolve(SpaceSafeURI loc) {
return new SpaceSafeURI(u.resolve(loc.u));
}
public SpaceSafeURI relativize(SpaceSafeURI loc) {
return new SpaceSafeURI(u.relativize(loc.u));
}
@Override
public boolean equals(Object obj) {
if (obj instanceof SpaceSafeURI) {
return u.equals(((SpaceSafeURI) obj).u);
}
return false;
}
@Override
public int hashCode() {
return u.hashCode();
}
@Override
public int compareTo(SpaceSafeURI uri) {
return u.compareTo(uri.u);
}
public String getScheme() {
return u.getScheme();
}
public String getAuthority() {
return u.getAuthority();
}
public String getHost() {
return u.getHost();
}
public URL toURL() throws MalformedURLException {
return u.toURL();
}
}

View File

@@ -1,26 +1,24 @@
package link.infra.packwiz.installer.metadata;
import java.lang.reflect.Type;
import java.net.URI;
import java.net.URISyntaxException;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import java.lang.reflect.Type;
import java.net.URISyntaxException;
/**
* This class encodes spaces before parsing the URI, so the URI can actually be
* parsed.
*/
class SpaceSafeURIParser implements JsonDeserializer<URI> {
class SpaceSafeURIParser implements JsonDeserializer<SpaceSafeURI> {
@Override
public URI deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
public SpaceSafeURI deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
throws JsonParseException {
String uriString = json.getAsString().replace(" ", "%20");
try {
return new URI(uriString);
return new SpaceSafeURI(json.getAsString());
} catch (URISyntaxException e) {
throw new JsonParseException("Failed to parse URI", e);
}

View File

@@ -4,9 +4,9 @@ import okio.HashingSource;
import okio.Source;
public class HashingSourceHasher implements IHasher {
String type;
private String type;
public HashingSourceHasher(String type) {
HashingSourceHasher(String type) {
this.type = type;
}
@@ -15,7 +15,7 @@ public class HashingSourceHasher implements IHasher {
HashingSource delegateHashing;
HashingSourceHash value;
public HashingSourceGeneralHashingSource(HashingSource delegate) {
HashingSourceGeneralHashingSource(HashingSource delegate) {
super(delegate);
delegateHashing = delegate;
}
@@ -46,9 +46,9 @@ public class HashingSourceHasher implements IHasher {
}
HashingSourceHash objHash = (HashingSourceHash) obj;
if (value != null) {
return value.equals(objHash.value);
return value.equalsIgnoreCase(objHash.value);
} else {
return objHash.value == null ? true : false;
return objHash.value == null;
}
}
@@ -71,9 +71,12 @@ public class HashingSourceHasher implements IHasher {
@Override
public GeneralHashingSource getHashingSource(Source delegate) {
switch (type) {
case "md5":
return new HashingSourceGeneralHashingSource(HashingSource.md5(delegate));
case "sha256":
return new HashingSourceGeneralHashingSource(HashingSource.sha256(delegate));
// TODO: support other hash types
return new HashingSourceGeneralHashingSource(HashingSource.sha256(delegate));
case "sha512":
return new HashingSourceGeneralHashingSource(HashingSource.sha512(delegate));
}
throw new RuntimeException("Invalid hash type provided");
}

View File

@@ -1,10 +1,10 @@
package link.infra.packwiz.installer.metadata.hash;
import java.io.IOException;
import okio.Buffer;
import okio.Source;
import java.io.IOException;
public class Murmur2Hasher implements IHasher {
private class Murmur2GeneralHashingSource extends GeneralHashingSource {
Murmur2Hash value;
@@ -40,8 +40,7 @@ public class Murmur2Hasher implements IHasher {
private byte[] computeNormalizedArray(byte[] input) {
byte[] output = new byte[input.length];
int num = 0;
for (int i = 0; i < input.length; i++) {
byte b = input[i];
for (byte b : input) {
if (!(b == 9 || b == 10 || b == 13 || b == 32)) {
output[num] = b;
num++;
@@ -54,7 +53,7 @@ public class Murmur2Hasher implements IHasher {
}
private class Murmur2Hash extends Hash {
private static class Murmur2Hash extends Hash {
int value;
private Murmur2Hash(String value) {
// Parsing as long then casting to int converts values gt int max value but lt uint max value

View File

@@ -1,23 +1,23 @@
package link.infra.packwiz.installer.request;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import link.infra.packwiz.installer.metadata.SpaceSafeURI;
import link.infra.packwiz.installer.request.handlers.RequestHandlerGithub;
import link.infra.packwiz.installer.request.handlers.RequestHandlerHTTP;
import okio.Source;
import java.util.ArrayList;
import java.util.List;
public abstract class HandlerManager {
public static List<IRequestHandler> handlers = new ArrayList<IRequestHandler>();
private static List<IRequestHandler> handlers = new ArrayList<>();
static {
handlers.add(new RequestHandlerGithub());
handlers.add(new RequestHandlerHTTP());
}
public static URI getNewLoc(URI base, URI loc) {
public static SpaceSafeURI getNewLoc(SpaceSafeURI base, SpaceSafeURI loc) {
if (loc == null) return null;
if (base != null) {
loc = base.resolve(loc);
@@ -35,7 +35,7 @@ public abstract class HandlerManager {
// Zip handler discards once read, requesting multiple times on other handlers would cause multiple downloads
// Caching system? Copy from already downloaded files?
public static Source getFileSource(URI loc) throws Exception {
public static Source getFileSource(SpaceSafeURI loc) throws Exception {
for (IRequestHandler handler : handlers) {
if (handler.matchesHandler(loc)) {
Source src = handler.getFileSource(loc);

View File

@@ -1,7 +1,6 @@
package link.infra.packwiz.installer.request;
import java.net.URI;
import link.infra.packwiz.installer.metadata.SpaceSafeURI;
import okio.Source;
/**
@@ -9,9 +8,9 @@ import okio.Source;
*/
public interface IRequestHandler {
public boolean matchesHandler(URI loc);
boolean matchesHandler(SpaceSafeURI loc);
public default URI getNewLoc(URI loc) {
default SpaceSafeURI getNewLoc(SpaceSafeURI loc) {
return loc;
}
@@ -20,8 +19,8 @@ public interface IRequestHandler {
* It is assumed that each location is read only once for the duration of an IRequestHandler.
* @param loc The location to be read
* @return The Source containing the data of the file
* @throws Exception
* @throws Exception Exception if it failed to download a file!!!
*/
public Source getFileSource(URI loc) throws Exception;
Source getFileSource(SpaceSafeURI loc) throws Exception;
}

View File

@@ -1,6 +1,7 @@
package link.infra.packwiz.installer.request.handlers;
import java.net.URI;
import link.infra.packwiz.installer.metadata.SpaceSafeURI;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;
@@ -14,38 +15,41 @@ public class RequestHandlerGithub extends RequestHandlerZip {
}
@Override
public URI getNewLoc(URI loc) {
public SpaceSafeURI getNewLoc(SpaceSafeURI loc) {
return loc;
}
// TODO: is caching really needed, if HTTPURLConnection follows redirects correctly?
private Map<String, URI> zipUriMap = new HashMap<String, URI>();
final ReentrantReadWriteLock zipUriLock = new ReentrantReadWriteLock();
private Map<String, SpaceSafeURI> zipUriMap = new HashMap<>();
private final ReentrantReadWriteLock zipUriLock = new ReentrantReadWriteLock();
private static Pattern repoMatcherPattern = Pattern.compile("/([\\w.-]+/[\\w.-]+).*");
private String getRepoName(URI loc) {
private String getRepoName(SpaceSafeURI loc) {
Matcher matcher = repoMatcherPattern.matcher(loc.getPath());
matcher.matches();
return matcher.group(1);
if (matcher.matches()) {
return matcher.group(1);
} else {
return null;
}
}
@Override
protected URI getZipUri(URI loc) throws Exception {
protected SpaceSafeURI getZipUri(SpaceSafeURI loc) throws Exception {
String repoName = getRepoName(loc);
String branchName = getBranch(loc);
zipUriLock.readLock().lock();
URI zipUri = zipUriMap.get(repoName + "/" + branchName);
SpaceSafeURI zipUri = zipUriMap.get(repoName + "/" + branchName);
zipUriLock.readLock().unlock();
if (zipUri != null) {
return zipUri;
}
zipUri = new URI("https://api.github.com/repos/" + repoName + "/zipball/" + branchName);
zipUri = new SpaceSafeURI("https://api.github.com/repos/" + repoName + "/zipball/" + branchName);
zipUriLock.writeLock().lock();
// If another thread sets the value concurrently, use the value of the
// thread that first acquired the lock.
URI zipUriInserted = zipUriMap.putIfAbsent(repoName + "/" + branchName, zipUri);
SpaceSafeURI zipUriInserted = zipUriMap.putIfAbsent(repoName + "/" + branchName, zipUri);
if (zipUriInserted != null) {
zipUri = zipUriInserted;
}
@@ -55,20 +59,23 @@ public class RequestHandlerGithub extends RequestHandlerZip {
private static Pattern branchMatcherPattern = Pattern.compile("/[\\w.-]+/[\\w.-]+/blob/([\\w.-]+).*");
private String getBranch(URI loc) {
private String getBranch(SpaceSafeURI loc) {
Matcher matcher = branchMatcherPattern.matcher(loc.getPath());
matcher.matches();
return matcher.group(1);
if (matcher.matches()) {
return matcher.group(1);
} else {
return null;
}
}
@Override
protected URI getLocationInZip(URI loc) throws Exception {
protected SpaceSafeURI getLocationInZip(SpaceSafeURI loc) throws Exception {
String path = "/" + getRepoName(loc) + "/blob/" + getBranch(loc);
return new URI(loc.getScheme(), loc.getAuthority(), path, null, null).relativize(loc);
return new SpaceSafeURI(loc.getScheme(), loc.getAuthority(), path, null, null).relativize(loc);
}
@Override
public boolean matchesHandler(URI loc) {
public boolean matchesHandler(SpaceSafeURI loc) {
String scheme = loc.getScheme();
if (!("http".equals(scheme) || "https".equals(scheme))) {
return false;

View File

@@ -1,22 +1,22 @@
package link.infra.packwiz.installer.request.handlers;
import java.net.URI;
import java.net.URLConnection;
import link.infra.packwiz.installer.metadata.SpaceSafeURI;
import link.infra.packwiz.installer.request.IRequestHandler;
import okio.Okio;
import okio.Source;
import java.net.URLConnection;
public class RequestHandlerHTTP implements IRequestHandler {
@Override
public boolean matchesHandler(URI loc) {
public boolean matchesHandler(SpaceSafeURI loc) {
String scheme = loc.getScheme();
return "http".equals(scheme) || "https".equals(scheme);
}
@Override
public Source getFileSource(URI loc) throws Exception {
public Source getFileSource(SpaceSafeURI loc) throws Exception {
URLConnection conn = loc.toURL().openConnection();
// TODO: when do we send specific headers??? should there be a way to signal this?
// github *sometimes* requires it, sometimes not!

View File

@@ -1,7 +1,12 @@
package link.infra.packwiz.installer.request.handlers;
import link.infra.packwiz.installer.metadata.SpaceSafeURI;
import okio.Buffer;
import okio.BufferedSource;
import okio.Okio;
import okio.Source;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;
@@ -11,14 +16,9 @@ import java.util.function.Predicate;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import okio.Buffer;
import okio.BufferedSource;
import okio.Okio;
import okio.Source;
public abstract class RequestHandlerZip extends RequestHandlerHTTP {
protected final boolean modeHasFolder;
private final boolean modeHasFolder;
public RequestHandlerZip(boolean modeHasFolder) {
this.modeHasFolder = modeHasFolder;
@@ -35,14 +35,14 @@ public abstract class RequestHandlerZip extends RequestHandlerHTTP {
private class ZipReader {
private final ZipInputStream zis;
private final Map<URI, Buffer> readFiles = new HashMap<URI, Buffer>();
private final Map<SpaceSafeURI, Buffer> readFiles = new HashMap<>();
// Write lock implies access to ZipInputStream - only 1 thread must read at a time!
final ReentrantLock filesLock = new ReentrantLock();
private ZipEntry entry;
private final BufferedSource zipSource;
public ZipReader(Source zip) {
ZipReader(Source zip) {
zis = new ZipInputStream(Okio.buffer(zip).inputStream());
zipSource = Okio.buffer(Okio.source(zis));
}
@@ -55,14 +55,14 @@ public abstract class RequestHandlerZip extends RequestHandlerHTTP {
}
// File lock must be obtained before calling this function
private Buffer findFile(URI loc) throws IOException, URISyntaxException {
private Buffer findFile(SpaceSafeURI loc) throws IOException, URISyntaxException {
while (true) {
entry = zis.getNextEntry();
if (entry == null) {
return null;
}
Buffer data = readCurrFile();
URI fileLoc = new URI(removeFolder(entry.getName()));
SpaceSafeURI fileLoc = new SpaceSafeURI(removeFolder(entry.getName()));
if (loc.equals(fileLoc)) {
return data;
} else {
@@ -71,7 +71,7 @@ public abstract class RequestHandlerZip extends RequestHandlerHTTP {
}
}
public Source getFileSource(URI loc) throws Exception {
Source getFileSource(SpaceSafeURI loc) throws Exception {
filesLock.lock();
// Assume files are only read once, allow GC by removing
Buffer file = readFiles.remove(loc);
@@ -82,15 +82,12 @@ public abstract class RequestHandlerZip extends RequestHandlerHTTP {
file = findFile(loc);
filesLock.unlock();
if (file != null) {
return file;
}
return null;
return file;
}
public URI findInZip(Predicate<URI> matches) throws Exception {
SpaceSafeURI findInZip(Predicate<SpaceSafeURI> matches) throws Exception {
filesLock.lock();
for (URI file : readFiles.keySet()) {
for (SpaceSafeURI file : readFiles.keySet()) {
if (matches.test(file)) {
filesLock.unlock();
return file;
@@ -104,7 +101,7 @@ public abstract class RequestHandlerZip extends RequestHandlerHTTP {
return null;
}
Buffer data = readCurrFile();
URI fileLoc = new URI(removeFolder(entry.getName()));
SpaceSafeURI fileLoc = new SpaceSafeURI(removeFolder(entry.getName()));
readFiles.put(fileLoc, data);
if (matches.test(fileLoc)) {
filesLock.unlock();
@@ -115,19 +112,19 @@ public abstract class RequestHandlerZip extends RequestHandlerHTTP {
}
private final Map<URI, ZipReader> cache = new HashMap<URI, ZipReader>();
final ReentrantReadWriteLock cacheLock = new ReentrantReadWriteLock();
private final Map<SpaceSafeURI, ZipReader> cache = new HashMap<>();
private final ReentrantReadWriteLock cacheLock = new ReentrantReadWriteLock();
protected abstract URI getZipUri(URI loc) throws Exception;
protected abstract SpaceSafeURI getZipUri(SpaceSafeURI loc) throws Exception;
protected abstract URI getLocationInZip(URI loc) throws Exception;
protected abstract SpaceSafeURI getLocationInZip(SpaceSafeURI loc) throws Exception;
@Override
public abstract boolean matchesHandler(URI loc);
public abstract boolean matchesHandler(SpaceSafeURI loc);
@Override
public Source getFileSource(URI loc) throws Exception {
URI zipUri = getZipUri(loc);
public Source getFileSource(SpaceSafeURI loc) throws Exception {
SpaceSafeURI zipUri = getZipUri(loc);
cacheLock.readLock().lock();
ZipReader zr = cache.get(zipUri);
cacheLock.readLock().unlock();
@@ -150,8 +147,8 @@ public abstract class RequestHandlerZip extends RequestHandlerHTTP {
return zr.getFileSource(getLocationInZip(loc));
}
protected URI findInZip(URI loc, Predicate<URI> matches) throws Exception {
URI zipUri = getZipUri(loc);
protected SpaceSafeURI findInZip(SpaceSafeURI loc, Predicate<SpaceSafeURI> matches) throws Exception {
SpaceSafeURI zipUri = getZipUri(loc);
cacheLock.readLock().lock();
ZipReader zr = cache.get(zipUri);
cacheLock.readLock().unlock();

View File

@@ -1,5 +1,9 @@
package link.infra.packwiz.installer.ui;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
public class CLIHandler implements IUserInterface {
@Override
@@ -8,7 +12,7 @@ public class CLIHandler implements IUserInterface {
}
@Override
public void show() {}
public void show(InputStateHandler h) {}
@Override
public void submitProgress(InstallProgress progress) {
@@ -29,5 +33,23 @@ public class CLIHandler implements IUserInterface {
task.run();
System.out.println("Finished successfully!");
}
@Override
public Future<Boolean> showOptions(List<IOptionDetails> options) {
for (IOptionDetails opt : options) {
opt.setOptionValue(true);
System.out.println("Warning: accepting option " + opt.getName() + " as option choosing is not implemented in the CLI");
}
CompletableFuture<Boolean> future = new CompletableFuture<>();
future.complete(false); // Can't be cancelled!
return future;
}
@Override
public Future<IExceptionDetails.ExceptionListResult> showExceptions(List<IExceptionDetails> opts, int numTotal, boolean allowsIgnore) {
CompletableFuture<IExceptionDetails.ExceptionListResult> future = new CompletableFuture<>();
future.complete(IExceptionDetails.ExceptionListResult.CANCEL);
return future;
}
}

View File

@@ -0,0 +1,183 @@
package link.infra.packwiz.installer.ui;
import link.infra.packwiz.installer.ui.IExceptionDetails.ExceptionListResult;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import java.util.concurrent.CompletableFuture;
class ExceptionListWindow extends JDialog {
private static final long serialVersionUID = 1L;
private final JTextArea lblExceptionStacktrace;
/**
* Create the dialog.
*/
ExceptionListWindow(List<IExceptionDetails> eList, CompletableFuture<ExceptionListResult> future, int numTotal, boolean allowsIgnore, JFrame parentWindow) {
super(parentWindow, "Failed file downloads", true);
setBounds(100, 100, 540, 340);
setLocationRelativeTo(parentWindow);
getContentPane().setLayout(new BorderLayout());
{
JPanel errorPanel = new JPanel();
getContentPane().add(errorPanel, BorderLayout.NORTH);
{
JLabel lblWarning = new JLabel("One or more errors were encountered while installing the modpack!");
lblWarning.setIcon(UIManager.getIcon("OptionPane.warningIcon"));
errorPanel.add(lblWarning);
}
}
JPanel contentPanel = new JPanel();
contentPanel.setBorder(new EmptyBorder(5, 5, 5, 5));
getContentPane().add(contentPanel, BorderLayout.CENTER);
contentPanel.setLayout(new BorderLayout(0, 0));
{
JSplitPane splitPane = new JSplitPane();
splitPane.setResizeWeight(0.3);
contentPanel.add(splitPane);
{
lblExceptionStacktrace = new JTextArea("Select a file");
lblExceptionStacktrace.setBackground(UIManager.getColor("List.background"));
lblExceptionStacktrace.setOpaque(true);
lblExceptionStacktrace.setWrapStyleWord(true);
lblExceptionStacktrace.setLineWrap(true);
lblExceptionStacktrace.setEditable(false);
lblExceptionStacktrace.setFocusable(true);
lblExceptionStacktrace.setFont(UIManager.getFont("Label.font"));
lblExceptionStacktrace.setBorder(new EmptyBorder(5, 5, 5, 5));
JScrollPane scrollPane = new JScrollPane(lblExceptionStacktrace);
scrollPane.setBorder(new EmptyBorder(0, 0, 0, 0));
splitPane.setRightComponent(scrollPane);
}
{
JList<String> list = new JList<>();
list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
list.setBorder(new EmptyBorder(5, 5, 5, 5));
ExceptionListModel listModel = new ExceptionListModel(eList);
list.setModel(listModel);
list.addListSelectionListener(e -> {
int i = list.getSelectedIndex();
if (i > -1) {
StringWriter sw = new StringWriter();
listModel.getExceptionAt(i).printStackTrace(new PrintWriter(sw));
lblExceptionStacktrace.setText(sw.toString());
// Scroll to the top
lblExceptionStacktrace.setCaretPosition(0);
} else {
lblExceptionStacktrace.setText("Select a file");
}
});
JScrollPane scrollPane = new JScrollPane(list);
scrollPane.setBorder(new EmptyBorder(0, 0, 0, 0));
splitPane.setLeftComponent(scrollPane);
}
}
{
JPanel buttonPane = new JPanel();
getContentPane().add(buttonPane, BorderLayout.SOUTH);
buttonPane.setLayout(new BorderLayout(0, 0));
{
JPanel rightButtons = new JPanel();
buttonPane.add(rightButtons, BorderLayout.EAST);
{
JButton btnContinue = new JButton("Continue");
btnContinue.setToolTipText("Attempt to continue installing, excluding the failed downloads");
btnContinue.addActionListener(e -> {
future.complete(ExceptionListResult.CONTINUE);
ExceptionListWindow.this.dispose();
});
rightButtons.add(btnContinue);
}
{
JButton btnCancelLaunch = new JButton("Cancel launch");
btnCancelLaunch.setToolTipText("Stop launching the game");
btnCancelLaunch.addActionListener(e -> {
future.complete(ExceptionListResult.CANCEL);
ExceptionListWindow.this.dispose();
});
rightButtons.add(btnCancelLaunch);
}
{
JButton btnIgnoreUpdate = new JButton("Ignore update");
btnIgnoreUpdate.setEnabled(allowsIgnore);
btnIgnoreUpdate.setToolTipText("Start the game without attempting to update");
btnIgnoreUpdate.addActionListener(e -> {
future.complete(ExceptionListResult.IGNORE);
ExceptionListWindow.this.dispose();
});
rightButtons.add(btnIgnoreUpdate);
{
JLabel lblErrored = new JLabel(eList.size() + "/" + numTotal + " errored");
lblErrored.setHorizontalAlignment(SwingConstants.CENTER);
buttonPane.add(lblErrored, BorderLayout.CENTER);
}
{
JPanel leftButtons = new JPanel();
buttonPane.add(leftButtons, BorderLayout.WEST);
{
JButton btnReportIssue = new JButton("Report issue");
boolean supported = Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE);
btnReportIssue.setEnabled(supported);
if (supported) {
btnReportIssue.addActionListener(e -> {
try {
Desktop.getDesktop().browse(new URI("https://github.com/comp500/packwiz-installer/issues/new"));
} catch (IOException | URISyntaxException e1) {
// lol the button just won't work i guess
}
});
}
leftButtons.add(btnReportIssue);
}
}
}
}
}
addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
future.complete(ExceptionListResult.CANCEL);
}
@Override
public void windowClosed(WindowEvent e) {
// Just in case closing didn't get triggered - if something else called dispose() the
// future will have already completed
future.complete(ExceptionListResult.CANCEL);
}
});
}
private static class ExceptionListModel extends AbstractListModel<String> {
private static final long serialVersionUID = 1L;
private final List<IExceptionDetails> details;
ExceptionListModel(List<IExceptionDetails> details) {
this.details = details;
}
public int getSize() {
return details.size();
}
public String getElementAt(int index) {
return details.get(index).getName();
}
Exception getExceptionAt(int index) {
return details.get(index).getException();
}
}
}

View File

@@ -0,0 +1,10 @@
package link.infra.packwiz.installer.ui;
public interface IExceptionDetails {
Exception getException();
String getName();
enum ExceptionListResult {
CONTINUE, CANCEL, IGNORE
}
}

View File

@@ -0,0 +1,8 @@
package link.infra.packwiz.installer.ui;
public interface IOptionDetails {
String getName();
boolean getOptionValue();
String getOptionDescription();
void setOptionValue(boolean value);
}

View File

@@ -1,23 +1,38 @@
package link.infra.packwiz.installer.ui;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
public interface IUserInterface {
public void show();
void show(InputStateHandler handler);
public void handleException(Exception e);
/**
* This might not exit straight away, return after calling this!
*/
public default void handleExceptionAndExit(Exception e) {
void handleException(Exception e);
default void handleExceptionAndExit(Exception e) {
handleException(e);
System.exit(1);
};
public default void setTitle(String title) {};
}
public void submitProgress(InstallProgress progress);
default void setTitle(String title) {}
public void executeManager(Runnable task);
void submitProgress(InstallProgress progress);
void executeManager(Runnable task);
// Return true if the installation was cancelled!
Future<Boolean> showOptions(List<IOptionDetails> option);
Future<IExceptionDetails.ExceptionListResult> showExceptions(List<IExceptionDetails> opts, int numTotal, boolean allowsIgnore);
default void disableOptionsButton() {}
// Should not return CONTINUE
default Future<IExceptionDetails.ExceptionListResult> showCancellationDialog() {
CompletableFuture<IExceptionDetails.ExceptionListResult> future = new CompletableFuture<>();
future.complete(IExceptionDetails.ExceptionListResult.CANCEL);
return future;
}
}

View File

@@ -0,0 +1,22 @@
package link.infra.packwiz.installer.ui;
public class InputStateHandler {
private boolean optionsButtonPressed = false;
private boolean cancelButtonPressed = false;
synchronized void pressCancelButton() {
this.cancelButtonPressed = true;
}
synchronized void pressOptionsButton() {
this.optionsButtonPressed = true;
}
public synchronized boolean getCancelButton() {
return cancelButtonPressed;
}
public synchronized boolean getOptionsButton() {
return optionsButtonPressed;
}
}

View File

@@ -1,51 +1,42 @@
package link.infra.packwiz.installer.ui;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.EventQueue;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.UIManager;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import java.awt.*;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicBoolean;
public class InstallWindow implements IUserInterface {
private JFrame frmPackwizlauncher;
private JLabel lblProgresslabel;
private JProgressBar progressBar;
private InputStateHandler inputStateHandler;
private String title = "Updating modpack...";
private SwingWorkerButWithPublicPublish<Void, InstallProgress> worker;
private AtomicBoolean aboutToCrash = new AtomicBoolean();
private JButton btnOptions;
@Override
public void show() {
EventQueue.invokeLater(new Runnable() {
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
InstallWindow.this.initialize();
InstallWindow.this.frmPackwizlauncher.setVisible(true);
} catch (Exception e) {
e.printStackTrace();
}
public void show(InputStateHandler handler) {
this.inputStateHandler = handler;
EventQueue.invokeLater(() -> {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
initialize();
frmPackwizlauncher.setVisible(true);
} catch (Exception e) {
e.printStackTrace();
}
});
}
/**
* Initialize the contents of the frame.
* @wbp.parser.entryPoint
*/
private void initialize() {
frmPackwizlauncher = new JFrame();
@@ -53,43 +44,41 @@ public class InstallWindow implements IUserInterface {
frmPackwizlauncher.setBounds(100, 100, 493, 95);
frmPackwizlauncher.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frmPackwizlauncher.setLocationRelativeTo(null);
JPanel panel = new JPanel();
panel.setBorder(new EmptyBorder(10, 10, 10, 10));
frmPackwizlauncher.getContentPane().add(panel, BorderLayout.CENTER);
panel.setLayout(new BorderLayout(0, 0));
progressBar = new JProgressBar();
progressBar.setIndeterminate(true);
panel.add(progressBar, BorderLayout.CENTER);
lblProgresslabel = new JLabel("Loading...");
panel.add(lblProgresslabel, BorderLayout.SOUTH);
JPanel panel_1 = new JPanel();
panel_1.setBorder(new EmptyBorder(0, 5, 0, 5));
frmPackwizlauncher.getContentPane().add(panel_1, BorderLayout.EAST);
GridBagLayout gbl_panel_1 = new GridBagLayout();
panel_1.setLayout(gbl_panel_1);
JButton btnOptions = new JButton("Options...");
btnOptions = new JButton("Optional mods...");
btnOptions.addActionListener(e -> {
btnOptions.setText("Loading...");
btnOptions.setEnabled(false);
inputStateHandler.pressOptionsButton();
});
btnOptions.setAlignmentX(Component.CENTER_ALIGNMENT);
GridBagConstraints gbc_btnOptions = new GridBagConstraints();
gbc_btnOptions.gridx = 0;
gbc_btnOptions.gridy = 0;
panel_1.add(btnOptions, gbc_btnOptions);
JButton btnCancel = new JButton("Cancel");
btnCancel.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent arg0) {
if (worker != null) {
worker.cancel(true);
}
frmPackwizlauncher.dispose();
// TODO: show window to ask user what to do
System.out.println("Update process cancelled by user!");
System.exit(1);
}
btnCancel.addActionListener(e -> {
btnCancel.setEnabled(false);
inputStateHandler.pressCancelButton();
});
btnCancel.setAlignmentX(Component.CENTER_ALIGNMENT);
GridBagConstraints gbc_btnCancel = new GridBagConstraints();
@@ -101,10 +90,8 @@ public class InstallWindow implements IUserInterface {
@Override
public void handleException(Exception e) {
e.printStackTrace();
EventQueue.invokeLater(new Runnable() {
public void run() {
JOptionPane.showMessageDialog(null, "An error occurred: \n" + e.getClass().getCanonicalName() + ": " + e.getMessage(), title, JOptionPane.ERROR_MESSAGE);
}
EventQueue.invokeLater(() -> {
JOptionPane.showMessageDialog(null, "An error occurred: \n" + e.getClass().getCanonicalName() + ": " + e.getMessage(), title, JOptionPane.ERROR_MESSAGE);
});
}
@@ -113,28 +100,39 @@ public class InstallWindow implements IUserInterface {
e.printStackTrace();
// Used to prevent the done() handler of SwingWorker executing if the invokeLater hasn't happened yet
aboutToCrash.set(true);
EventQueue.invokeLater(new Runnable() {
public void run() {
JOptionPane.showMessageDialog(null, "A fatal error occurred: \n" + e.getClass().getCanonicalName() + ": " + e.getMessage(), title, JOptionPane.ERROR_MESSAGE);
System.exit(1);
}
EventQueue.invokeLater(() -> {
JOptionPane.showMessageDialog(null, "A fatal error occurred: \n" + e.getClass().getCanonicalName() + ": " + e.getMessage(), title, JOptionPane.ERROR_MESSAGE);
System.exit(1);
});
// Pause forever, so it blocks while we wait for System.exit to take effect
try {
Thread.currentThread().join();
} catch (InterruptedException ex) {
// no u
}
}
@Override
public void setTitle(String title) {
this.title = title;
if (frmPackwizlauncher != null) {
EventQueue.invokeLater(new Runnable() {
public void run() {
InstallWindow.this.frmPackwizlauncher.setTitle(title);
}
});
EventQueue.invokeLater(() -> frmPackwizlauncher.setTitle(title));
}
}
@Override
public void submitProgress(InstallProgress progress) {
StringBuilder sb = new StringBuilder();
if (progress.hasProgress) {
sb.append('(');
sb.append(progress.progress);
sb.append('/');
sb.append(progress.progressTotal);
sb.append(") ");
}
sb.append(progress.message);
// TODO: better logging library?
System.out.println(sb.toString());
if (worker != null) {
worker.publishPublic(progress);
}
@@ -142,48 +140,89 @@ public class InstallWindow implements IUserInterface {
@Override
public void executeManager(Runnable task) {
EventQueue.invokeLater(new Runnable() {
public void run() {
worker = new SwingWorkerButWithPublicPublish<Void, InstallProgress>() {
EventQueue.invokeLater(() -> {
worker = new SwingWorkerButWithPublicPublish<Void, InstallProgress>() {
@Override
protected Void doInBackground() throws Exception {
task.run();
return null;
}
@Override
protected Void doInBackground() {
task.run();
return null;
}
@Override
protected void process(List<InstallProgress> chunks) {
// Only process last chunk
if (chunks.size() > 0) {
InstallProgress prog = chunks.get(chunks.size() - 1);
if (prog.hasProgress) {
progressBar.setIndeterminate(false);
progressBar.setValue(prog.progress);
progressBar.setMaximum(prog.progressTotal);
} else {
progressBar.setIndeterminate(true);
progressBar.setValue(0);
}
lblProgresslabel.setText(prog.message);
@Override
protected void process(List<InstallProgress> chunks) {
// Only process last chunk
if (chunks.size() > 0) {
InstallProgress prog = chunks.get(chunks.size() - 1);
if (prog.hasProgress) {
progressBar.setIndeterminate(false);
progressBar.setValue(prog.progress);
progressBar.setMaximum(prog.progressTotal);
} else {
progressBar.setIndeterminate(true);
progressBar.setValue(0);
}
lblProgresslabel.setText(prog.message);
}
}
@Override
protected void done() {
if (aboutToCrash.get()) {
return;
}
// TODO: a better way to do this?
frmPackwizlauncher.dispose();
System.out.println("Finished successfully!");
System.exit(0);
@Override
protected void done() {
if (aboutToCrash.get()) {
return;
}
};
worker.execute();
}
// TODO: a better way to do this?
frmPackwizlauncher.dispose();
System.out.println("Finished successfully!");
System.exit(0);
}
};
worker.execute();
});
}
@Override
public Future<Boolean> showOptions(List<IOptionDetails> opts) {
CompletableFuture<Boolean> future = new CompletableFuture<>();
EventQueue.invokeLater(() -> {
OptionsSelectWindow dialog = new OptionsSelectWindow(opts, future, frmPackwizlauncher);
dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
dialog.setVisible(true);
});
return future;
}
@Override
public Future<IExceptionDetails.ExceptionListResult> showExceptions(List<IExceptionDetails> opts, int numTotal, boolean allowsIgnore) {
CompletableFuture<IExceptionDetails.ExceptionListResult> future = new CompletableFuture<>();
EventQueue.invokeLater(() -> {
ExceptionListWindow dialog = new ExceptionListWindow(opts, future, numTotal, allowsIgnore, frmPackwizlauncher);
dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
dialog.setVisible(true);
});
return future;
}
@Override
public void disableOptionsButton() {
if (btnOptions != null) {
btnOptions.setText("Optional mods...");
btnOptions.setEnabled(false);
}
}
@Override
public Future<IExceptionDetails.ExceptionListResult> showCancellationDialog() {
CompletableFuture<IExceptionDetails.ExceptionListResult> future = new CompletableFuture<>();
EventQueue.invokeLater(() -> {
Object[] buttons = {"Quit", "Ignore"};
int result = JOptionPane.showOptionDialog(frmPackwizlauncher,
"The installation was cancelled. Would you like to quit the game, or ignore the update and start the game?",
"Cancelled installation",
JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null, buttons, buttons[0]);
future.complete(result == 0 ? IExceptionDetails.ExceptionListResult.CANCEL : IExceptionDetails.ExceptionListResult.IGNORE);
});
return future;
}
}

View File

@@ -0,0 +1,33 @@
package link.infra.packwiz.installer.ui;
// Serves as a proxy for IOptionDetails, so that setOptionValue isn't called until OK is clicked
class OptionTempHandler implements IOptionDetails {
private final IOptionDetails opt;
private boolean tempValue;
OptionTempHandler(IOptionDetails opt) {
this.opt = opt;
tempValue = opt.getOptionValue();
}
public String getName() {
return opt.getName();
}
public String getOptionDescription() {
return opt.getOptionDescription();
}
public boolean getOptionValue() {
return tempValue;
}
public void setOptionValue(boolean value) {
tempValue = value;
}
void finalise() {
opt.setOptionValue(tempValue);
}
}

View File

@@ -0,0 +1,205 @@
package link.infra.packwiz.installer.ui;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.event.TableModelListener;
import javax.swing.table.TableModel;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
public class OptionsSelectWindow extends JDialog implements ActionListener {
private static final long serialVersionUID = 1L;
private final JTextArea lblOptionDescription;
private final OptionTableModel tableModel;
private final CompletableFuture<Boolean> future;
/**
* Create the dialog.
*/
OptionsSelectWindow(List<IOptionDetails> optList, CompletableFuture<Boolean> future, JFrame parentWindow) {
super(parentWindow, "Select optional mods...", true);
tableModel = new OptionTableModel(optList);
this.future = future;
setBounds(100, 100, 450, 300);
setLocationRelativeTo(parentWindow);
getContentPane().setLayout(new BorderLayout());
JPanel contentPanel = new JPanel();
contentPanel.setBorder(new EmptyBorder(5, 5, 5, 5));
getContentPane().add(contentPanel, BorderLayout.CENTER);
contentPanel.setLayout(new BorderLayout(0, 0));
{
JSplitPane splitPane = new JSplitPane();
splitPane.setResizeWeight(0.5);
contentPanel.add(splitPane);
{
JTable table = new JTable();
table.setShowVerticalLines(false);
table.setShowHorizontalLines(false);
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
table.setShowGrid(false);
table.setModel(tableModel);
table.getColumnModel().getColumn(0).setResizable(false);
table.getColumnModel().getColumn(0).setPreferredWidth(15);
table.getColumnModel().getColumn(0).setMaxWidth(15);
table.getColumnModel().getColumn(1).setResizable(false);
table.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
@Override
public void valueChanged(ListSelectionEvent e) {
int i = table.getSelectedRow();
if (i > -1) {
lblOptionDescription.setText(tableModel.getDescription(i));
} else {
lblOptionDescription.setText("Select an option...");
}
}
});
table.setTableHeader(null);
JScrollPane scrollPane = new JScrollPane(table);
scrollPane.getViewport().setBackground(UIManager.getColor("List.background"));
scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
scrollPane.setBorder(new EmptyBorder(0, 0, 0, 0));
splitPane.setLeftComponent(scrollPane);
}
{
lblOptionDescription = new JTextArea("Select an option...");
lblOptionDescription.setBackground(UIManager.getColor("List.background"));
lblOptionDescription.setOpaque(true);
lblOptionDescription.setWrapStyleWord(true);
lblOptionDescription.setLineWrap(true);
lblOptionDescription.setEditable(false);
lblOptionDescription.setFocusable(false);
lblOptionDescription.setFont(UIManager.getFont("Label.font"));
lblOptionDescription.setBorder(new EmptyBorder(10, 10, 10, 10));
JScrollPane scrollPane = new JScrollPane(lblOptionDescription);
scrollPane.setBorder(new EmptyBorder(0, 0, 0, 0));
splitPane.setRightComponent(scrollPane);
}
}
{
JPanel buttonPane = new JPanel();
buttonPane.setLayout(new FlowLayout(FlowLayout.RIGHT));
getContentPane().add(buttonPane, BorderLayout.SOUTH);
{
JButton okButton = new JButton("OK");
okButton.setActionCommand("OK");
okButton.addActionListener(this);
buttonPane.add(okButton);
getRootPane().setDefaultButton(okButton);
}
{
JButton cancelButton = new JButton("Cancel");
cancelButton.setActionCommand("Cancel");
cancelButton.addActionListener(this);
buttonPane.add(cancelButton);
}
}
addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
future.complete(true);
}
@Override
public void windowClosed(WindowEvent e) {
// Just in case closing didn't get triggered - if something else called dispose() the
// future will have already completed
future.complete(true);
}
});
}
private static class OptionTableModel implements TableModel {
private List<OptionTempHandler> opts = new ArrayList<>();
OptionTableModel(List<IOptionDetails> givenOpts) {
for (IOptionDetails opt : givenOpts) {
opts.add(new OptionTempHandler(opt));
}
}
@Override
public int getRowCount() {
return opts.size();
}
@Override
public int getColumnCount() {
return 2;
}
private final String[] columnNames = {"Enabled", "Mod name"};
private final Class<?>[] columnTypes = {Boolean.class, String.class};
private final boolean[] columnEditables = {true, false};
@Override
public String getColumnName(int columnIndex) {
return columnNames[columnIndex];
}
@Override
public Class<?> getColumnClass(int columnIndex) {
return columnTypes[columnIndex];
}
@Override
public boolean isCellEditable(int rowIndex, int columnIndex) {
return columnEditables[columnIndex];
}
@Override
public Object getValueAt(int rowIndex, int columnIndex) {
OptionTempHandler opt = opts.get(rowIndex);
return columnIndex == 0 ? opt.getOptionValue() : opt.getName();
}
@Override
public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
if (columnIndex == 0) {
OptionTempHandler opt = opts.get(rowIndex);
opt.setOptionValue((boolean) aValue);
}
}
// Noop, the table model doesn't change!
@Override
public void addTableModelListener(TableModelListener l) {}
@Override
public void removeTableModelListener(TableModelListener l) {}
String getDescription(int rowIndex) {
return opts.get(rowIndex).getOptionDescription();
}
void finalise() {
for (OptionTempHandler opt : opts) {
opt.finalise();
}
}
}
@Override
public void actionPerformed(ActionEvent e) {
if (e.getActionCommand().equals("OK")) {
tableModel.finalise();
future.complete(false);
dispose();
} else if (e.getActionCommand().equals("Cancel")) {
future.complete(true);
dispose();
}
}
}