mirror of
https://github.com/packwiz/packwiz-installer.git
synced 2025-10-16 16:04:32 +02:00
Compare commits
32 Commits
v0.0.4-pre
...
v0.1.0
Author | SHA1 | Date | |
---|---|---|---|
|
d21668afa6 | ||
|
7946377159 | ||
|
5a54a90f59 | ||
|
465e4973ba | ||
|
ea60175514 | ||
|
78f5d76fe9 | ||
|
452ab15cc7 | ||
|
87b00f316a | ||
|
1623c0f880 | ||
|
46bbc9b82e | ||
|
02b50be782 | ||
|
e6637b9af8 | ||
|
5fc7d6382d | ||
|
afd8e85754 | ||
|
ecbf0b9eba | ||
|
37a1464e11 | ||
|
54fd84a6d8 | ||
|
dcf8d21aad | ||
|
eaed3b2187 | ||
|
b22edf920e | ||
|
4d8e695fc4 | ||
|
a9bd83e96b | ||
|
ae085743be | ||
|
ad79cb3b21 | ||
|
320e56e74e | ||
|
bd95bc15ad | ||
|
794b817eff | ||
|
a5ff63c587 | ||
|
34a86ffb7d | ||
|
780efe2c9f | ||
|
d1647764c4 | ||
|
12bf090895 |
119
.gitignore
vendored
119
.gitignore
vendored
@@ -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 ###
|
# Created by https://www.gitignore.io/api/java,gradle,intellij
|
||||||
.metadata
|
# Edit at https://www.gitignore.io/?templates=java,gradle,intellij
|
||||||
bin/
|
|
||||||
tmp/
|
|
||||||
*.tmp
|
|
||||||
*.bak
|
|
||||||
*.swp
|
|
||||||
*~.nib
|
|
||||||
local.properties
|
|
||||||
.settings/
|
|
||||||
.loadpath
|
|
||||||
.recommenders
|
|
||||||
|
|
||||||
# External tool builders
|
### Intellij ###
|
||||||
.externalToolBuilders/
|
# 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"
|
# User-specific stuff
|
||||||
*.launch
|
.idea/**/workspace.xml
|
||||||
|
.idea/**/tasks.xml
|
||||||
|
.idea/**/usage.statistics.xml
|
||||||
|
.idea/**/dictionaries
|
||||||
|
.idea/**/shelf
|
||||||
|
|
||||||
# PyDev specific (Python IDE for Eclipse)
|
# Generated files
|
||||||
*.pydevproject
|
.idea/**/contentModel.xml
|
||||||
|
|
||||||
# CDT-specific (C/C++ Development Tooling)
|
# Sensitive or high-churn files
|
||||||
.cproject
|
.idea/**/dataSources/
|
||||||
|
.idea/**/dataSources.ids
|
||||||
|
.idea/**/dataSources.local.xml
|
||||||
|
.idea/**/sqlDataSources.xml
|
||||||
|
.idea/**/dynamic.xml
|
||||||
|
.idea/**/uiDesigner.xml
|
||||||
|
.idea/**/dbnavigator.xml
|
||||||
|
|
||||||
# CDT- autotools
|
# Gradle
|
||||||
.autotools
|
.idea/**/gradle.xml
|
||||||
|
.idea/**/libraries
|
||||||
|
|
||||||
# Java annotation processor (APT)
|
# Gradle and Maven with auto-import
|
||||||
.factorypath
|
# 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)
|
# CMake
|
||||||
.buildpath
|
cmake-build-*/
|
||||||
|
|
||||||
# sbteclipse plugin
|
# Mongo Explorer plugin
|
||||||
.target
|
.idea/**/mongoSettings.xml
|
||||||
|
|
||||||
# Tern plugin
|
# File-based project format
|
||||||
.tern-project
|
*.iws
|
||||||
|
|
||||||
# TeXlipse plugin
|
# IntelliJ
|
||||||
.texlipse
|
out/
|
||||||
|
|
||||||
# STS (Spring Tool Suite)
|
# mpeltonen/sbt-idea plugin
|
||||||
.springBeans
|
.idea_modules/
|
||||||
|
|
||||||
# Code Recommenders
|
# JIRA plugin
|
||||||
.recommenders/
|
atlassian-ide-plugin.xml
|
||||||
|
|
||||||
# Annotation Processing
|
# Cursive Clojure plugin
|
||||||
.apt_generated/
|
.idea/replstate.xml
|
||||||
|
|
||||||
# Scala IDE specific (Scala & Java development for Eclipse)
|
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||||
.cache-main
|
com_crashlytics_export_strings.xml
|
||||||
.scala_dependencies
|
crashlytics.properties
|
||||||
.worksheet
|
crashlytics-build.properties
|
||||||
|
fabric.properties
|
||||||
|
|
||||||
### Eclipse Patch ###
|
# Editor-based Rest Client
|
||||||
# Eclipse Core
|
.idea/httpRequests
|
||||||
.project
|
|
||||||
|
|
||||||
# JDT-specific (Eclipse Java Development Tools)
|
# Android studio 3.1+ serialized cache file
|
||||||
.classpath
|
.idea/caches/build_file_checksums.ser
|
||||||
|
|
||||||
# Annotation Processing
|
### Intellij Patch ###
|
||||||
.apt_generated
|
# 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 ###
|
### Java ###
|
||||||
# Compiled class file
|
# Compiled class file
|
||||||
@@ -114,4 +127,4 @@ gradle-app.setting
|
|||||||
### Gradle Patch ###
|
### Gradle Patch ###
|
||||||
**/build/
|
**/build/
|
||||||
|
|
||||||
# End of https://www.gitignore.io/api/java,gradle,eclipse
|
# End of https://www.gitignore.io/api/java,gradle,intellij
|
36
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
36
.idea/inspectionProfiles/Project_Default.xml
generated
Normal 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
10
.idea/misc.xml
generated
Normal 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
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
9
.vscode/settings.json
vendored
9
.vscode/settings.json
vendored
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"files.exclude": {
|
|
||||||
"**/.classpath": true,
|
|
||||||
"**/.project": true,
|
|
||||||
"**/.settings": true,
|
|
||||||
"**/.factorypath": true
|
|
||||||
},
|
|
||||||
"java.configuration.updateBuildConfiguration": "interactive"
|
|
||||||
}
|
|
@@ -5,6 +5,8 @@ plugins {
|
|||||||
id 'com.palantir.git-version' version '0.11.0'
|
id 'com.palantir.git-version' version '0.11.0'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sourceCompatibility = 1.8
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'commons-cli:commons-cli:1.4'
|
implementation 'commons-cli:commons-cli:1.4'
|
||||||
implementation 'com.moandjiezana.toml:toml4j:0.7.2'
|
implementation 'com.moandjiezana.toml:toml4j:0.7.2'
|
||||||
|
244
src/main/java/link/infra/packwiz/installer/DownloadTask.java
Normal file
244
src/main/java/link/infra/packwiz/installer/DownloadTask.java
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (Files.exists(destPath)) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@@ -1,40 +1,32 @@
|
|||||||
package link.infra.packwiz.installer;
|
package link.infra.packwiz.installer;
|
||||||
|
|
||||||
import java.awt.EventQueue;
|
import link.infra.packwiz.installer.metadata.SpaceSafeURI;
|
||||||
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.ui.CLIHandler;
|
import link.infra.packwiz.installer.ui.CLIHandler;
|
||||||
import link.infra.packwiz.installer.ui.IUserInterface;
|
import link.infra.packwiz.installer.ui.IUserInterface;
|
||||||
|
import link.infra.packwiz.installer.ui.InputStateHandler;
|
||||||
import link.infra.packwiz.installer.ui.InstallWindow;
|
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 {
|
public class Main {
|
||||||
|
|
||||||
// Actual main() is in RequiresBootstrap!
|
// Actual main() is in RequiresBootstrap!
|
||||||
|
@SuppressWarnings("unused")
|
||||||
public Main(String[] args) {
|
public Main(String[] args) {
|
||||||
// Big overarching try/catch just in case everything breaks
|
// Big overarching try/catch just in case everything breaks
|
||||||
try {
|
try {
|
||||||
this.startup(args);
|
this.startup(args);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
EventQueue.invokeLater(new Runnable() {
|
EventQueue.invokeLater(() -> {
|
||||||
public void run() {
|
JOptionPane.showMessageDialog(null,
|
||||||
JOptionPane.showMessageDialog(null,
|
"A fatal error occurred: \n" + e.getClass().getCanonicalName() + ": " + e.getMessage(),
|
||||||
"A fatal error occurred: \n" + e.getClass().getCanonicalName() + ": " + e.getMessage(),
|
"packwiz-installer", JOptionPane.ERROR_MESSAGE);
|
||||||
"packwiz-installer", JOptionPane.ERROR_MESSAGE);
|
System.exit(1);
|
||||||
System.exit(1);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
// In case the eventqueue is broken, exit after 1 minute
|
// In case the eventqueue is broken, exit after 1 minute
|
||||||
try {
|
try {
|
||||||
@@ -47,7 +39,7 @@ public class Main {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void startup(String[] args) {
|
private void startup(String[] args) {
|
||||||
Options options = new Options();
|
Options options = new Options();
|
||||||
addNonBootstrapOptions(options);
|
addNonBootstrapOptions(options);
|
||||||
addBootstrapOptions(options);
|
addBootstrapOptions(options);
|
||||||
@@ -89,7 +81,8 @@ public class Main {
|
|||||||
ui.setTitle(title);
|
ui.setTitle(title);
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.show();
|
InputStateHandler inputStateHandler = new InputStateHandler();
|
||||||
|
ui.show(inputStateHandler);
|
||||||
|
|
||||||
UpdateManager.Options uOptions = new UpdateManager.Options();
|
UpdateManager.Options uOptions = new UpdateManager.Options();
|
||||||
|
|
||||||
@@ -109,7 +102,7 @@ public class Main {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
uOptions.downloadURI = new URI(unparsedArgs[0]);
|
uOptions.downloadURI = new SpaceSafeURI(unparsedArgs[0]);
|
||||||
} catch (URISyntaxException e) {
|
} catch (URISyntaxException e) {
|
||||||
// TODO: better error message?
|
// TODO: better error message?
|
||||||
ui.handleExceptionAndExit(e);
|
ui.handleExceptionAndExit(e);
|
||||||
@@ -119,26 +112,22 @@ public class Main {
|
|||||||
// Start update process!
|
// Start update process!
|
||||||
// TODO: start in SwingWorker?
|
// TODO: start in SwingWorker?
|
||||||
try {
|
try {
|
||||||
ui.executeManager(new Runnable(){
|
ui.executeManager(() -> {
|
||||||
@Override
|
try {
|
||||||
public void run() {
|
new UpdateManager(uOptions, ui, inputStateHandler);
|
||||||
try {
|
} catch (Exception e) {
|
||||||
new UpdateManager(uOptions, ui);
|
// TODO: better error message?
|
||||||
} catch (Exception e) {
|
ui.handleExceptionAndExit(e);
|
||||||
// TODO: better error message?
|
|
||||||
ui.handleExceptionAndExit(e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// TODO: better error message?
|
// TODO: better error message?
|
||||||
ui.handleExceptionAndExit(e);
|
ui.handleExceptionAndExit(e);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Called by packwiz-installer-bootstrap to set up the help command
|
// Called by packwiz-installer-bootstrap to set up the help command
|
||||||
|
@SuppressWarnings("WeakerAccess")
|
||||||
public static void addNonBootstrapOptions(Options options) {
|
public static void addNonBootstrapOptions(Options options) {
|
||||||
options.addOption("s", "side", true, "Side to install mods from (client/server, defaults to client)");
|
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");
|
options.addOption(null, "title", true, "Title of the installer window");
|
||||||
|
@@ -1,10 +1,8 @@
|
|||||||
package link.infra.packwiz.installer;
|
package link.infra.packwiz.installer;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
||||||
import javax.swing.JOptionPane;
|
|
||||||
import javax.swing.UIManager;
|
|
||||||
|
|
||||||
public class RequiresBootstrap {
|
public class RequiresBootstrap {
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
@@ -15,10 +13,10 @@ public class RequiresBootstrap {
|
|||||||
if (Arrays.stream(args).map(str -> {
|
if (Arrays.stream(args).map(str -> {
|
||||||
if (str == null) return "";
|
if (str == null) return "";
|
||||||
if (str.startsWith("--")) {
|
if (str.startsWith("--")) {
|
||||||
return str.substring(2, str.length());
|
return str.substring(2);
|
||||||
}
|
}
|
||||||
if (str.startsWith("-")) {
|
if (str.startsWith("-")) {
|
||||||
return str.substring(1, str.length());
|
return str.substring(1);
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
}).anyMatch(str -> str.equals("g") || str.equals("no-gui"))) {
|
}).anyMatch(str -> str.equals("g") || str.equals("no-gui"))) {
|
||||||
|
@@ -1,58 +1,53 @@
|
|||||||
package link.infra.packwiz.installer;
|
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.Gson;
|
||||||
|
import com.google.gson.GsonBuilder;
|
||||||
import com.google.gson.JsonIOException;
|
import com.google.gson.JsonIOException;
|
||||||
import com.google.gson.JsonSyntaxException;
|
import com.google.gson.JsonSyntaxException;
|
||||||
import com.google.gson.annotations.SerializedName;
|
import com.google.gson.annotations.SerializedName;
|
||||||
import com.moandjiezana.toml.Toml;
|
import com.moandjiezana.toml.Toml;
|
||||||
|
|
||||||
import link.infra.packwiz.installer.metadata.IndexFile;
|
import link.infra.packwiz.installer.metadata.IndexFile;
|
||||||
import link.infra.packwiz.installer.metadata.ManifestFile;
|
import link.infra.packwiz.installer.metadata.ManifestFile;
|
||||||
import link.infra.packwiz.installer.metadata.PackFile;
|
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.GeneralHashingSource;
|
||||||
|
import link.infra.packwiz.installer.metadata.hash.Hash;
|
||||||
import link.infra.packwiz.installer.metadata.hash.HashUtils;
|
import link.infra.packwiz.installer.metadata.hash.HashUtils;
|
||||||
import link.infra.packwiz.installer.request.HandlerManager;
|
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.IUserInterface;
|
||||||
|
import link.infra.packwiz.installer.ui.InputStateHandler;
|
||||||
import link.infra.packwiz.installer.ui.InstallProgress;
|
import link.infra.packwiz.installer.ui.InstallProgress;
|
||||||
import okio.Buffer;
|
|
||||||
import okio.Okio;
|
import okio.Okio;
|
||||||
import okio.Source;
|
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 class UpdateManager {
|
||||||
|
|
||||||
public final Options opts;
|
private final Options opts;
|
||||||
public final IUserInterface ui;
|
public final IUserInterface ui;
|
||||||
|
private boolean cancelled;
|
||||||
|
private boolean cancelledStartGame = false;
|
||||||
|
private InputStateHandler stateHandler;
|
||||||
|
|
||||||
public static class Options {
|
public static class Options {
|
||||||
public URI downloadURI = null;
|
SpaceSafeURI downloadURI = null;
|
||||||
public String manifestFile = "packwiz.json"; // TODO: make configurable
|
String manifestFile = "packwiz.json"; // TODO: make configurable
|
||||||
public String packFolder = ".";
|
String packFolder = ".";
|
||||||
public Side side = Side.CLIENT;
|
Side side = Side.CLIENT;
|
||||||
|
|
||||||
public static enum Side {
|
public enum Side {
|
||||||
@SerializedName("client")
|
@SerializedName("client")
|
||||||
CLIENT("client"), @SerializedName("server")
|
CLIENT("client"),
|
||||||
SERVER("server"), @SerializedName("both")
|
@SerializedName("server")
|
||||||
|
SERVER("server"),
|
||||||
|
@SerializedName("both")
|
||||||
BOTH("both", new Side[] { CLIENT, SERVER });
|
BOTH("both", new Side[] { CLIENT, SERVER });
|
||||||
|
|
||||||
private final String sideName;
|
private final String sideName;
|
||||||
@@ -78,8 +73,8 @@ public class UpdateManager {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (this.depSides != null) {
|
if (this.depSides != null) {
|
||||||
for (int i = 0; i < this.depSides.length; i++) {
|
for (Side depSide : this.depSides) {
|
||||||
if (this.depSides[i].equals(tSide)) {
|
if (depSide.equals(tSide)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -90,7 +85,7 @@ public class UpdateManager {
|
|||||||
public static Side from(String name) {
|
public static Side from(String name) {
|
||||||
String lowerName = name.toLowerCase();
|
String lowerName = name.toLowerCase();
|
||||||
for (Side side : Side.values()) {
|
for (Side side : Side.values()) {
|
||||||
if (side.sideName == lowerName) {
|
if (side.sideName.equals(lowerName)) {
|
||||||
return side;
|
return side;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -99,17 +94,18 @@ public class UpdateManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public UpdateManager(Options opts, IUserInterface ui) {
|
UpdateManager(Options opts, IUserInterface ui, InputStateHandler inputStateHandler) {
|
||||||
this.opts = opts;
|
this.opts = opts;
|
||||||
this.ui = ui;
|
this.ui = ui;
|
||||||
|
this.stateHandler = inputStateHandler;
|
||||||
this.start();
|
this.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void start() {
|
private void start() {
|
||||||
this.checkOptions();
|
this.checkOptions();
|
||||||
|
|
||||||
ui.submitProgress(new InstallProgress("Loading manifest file..."));
|
ui.submitProgress(new InstallProgress("Loading manifest file..."));
|
||||||
Gson gson = new Gson();
|
Gson gson = new GsonBuilder().registerTypeAdapter(Hash.class, new Hash.TypeHandler()).create();
|
||||||
ManifestFile manifest;
|
ManifestFile manifest;
|
||||||
try {
|
try {
|
||||||
manifest = gson.fromJson(new FileReader(Paths.get(opts.packFolder, opts.manifestFile).toString()),
|
manifest = gson.fromJson(new FileReader(Paths.get(opts.packFolder, opts.manifestFile).toString()),
|
||||||
@@ -121,6 +117,11 @@ public class UpdateManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (stateHandler.getCancelButton()) {
|
||||||
|
showCancellationDialog();
|
||||||
|
handleCancellation();
|
||||||
|
}
|
||||||
|
|
||||||
ui.submitProgress(new InstallProgress("Loading pack file..."));
|
ui.submitProgress(new InstallProgress("Loading pack file..."));
|
||||||
GeneralHashingSource packFileSource;
|
GeneralHashingSource packFileSource;
|
||||||
try {
|
try {
|
||||||
@@ -140,25 +141,66 @@ public class UpdateManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (manifest.packFileHash != null && packFileSource.hashIsEqual(manifest.packFileHash)) {
|
if (stateHandler.getCancelButton()) {
|
||||||
System.out.println("Hash already up to date!");
|
showCancellationDialog();
|
||||||
// WOOO it's already up to date
|
handleCancellation();
|
||||||
// todo: --force?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
System.out.println(pf.name);
|
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()) {
|
||||||
|
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 (!Files.exists(Paths.get(opts.packFolder, entry.getValue().cachedLocation))) {
|
||||||
|
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?
|
||||||
|
if (!stateHandler.getOptionsButton()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
System.out.println("Modpack name: " + pf.name);
|
||||||
|
|
||||||
|
if (stateHandler.getCancelButton()) {
|
||||||
|
showCancellationDialog();
|
||||||
|
handleCancellation();
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// This is badly written, I'll probably heavily refactor it at some point
|
||||||
processIndex(HandlerManager.getNewLoc(opts.downloadURI, pf.index.file),
|
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) {
|
} catch (Exception e1) {
|
||||||
ui.handleExceptionAndExit(e1);
|
ui.handleExceptionAndExit(e1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// When successfully updated
|
handleCancellation();
|
||||||
|
|
||||||
|
// TODO: update MMC params, java args etc
|
||||||
|
|
||||||
manifest.packFileHash = packFileSource.getHash();
|
manifest.packFileHash = packFileSource.getHash();
|
||||||
// update other hashes
|
manifest.cachedSide = opts.side;
|
||||||
// TODO: don't do this on failure?
|
|
||||||
try (Writer writer = new FileWriter(Paths.get(opts.packFolder, opts.manifestFile).toString())) {
|
try (Writer writer = new FileWriter(Paths.get(opts.packFolder, opts.manifestFile).toString())) {
|
||||||
gson.toJson(manifest, writer);
|
gson.toJson(manifest, writer);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
@@ -168,11 +210,19 @@ public class UpdateManager {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void checkOptions() {
|
private void checkOptions() {
|
||||||
// TODO: implement
|
// TODO: implement
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void processIndex(URI indexUri, Object indexHash, String hashFormat, ManifestFile manifest) {
|
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!");
|
||||||
|
if (!stateHandler.getOptionsButton()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
manifest.indexFileHash = indexHash;
|
||||||
|
|
||||||
GeneralHashingSource indexFileSource;
|
GeneralHashingSource indexFileSource;
|
||||||
try {
|
try {
|
||||||
Source src = HandlerManager.getFileSource(indexUri);
|
Source src = HandlerManager.getFileSource(indexUri);
|
||||||
@@ -192,173 +242,238 @@ public class UpdateManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!indexFileSource.hashIsEqual(indexHash)) {
|
if (!indexFileSource.hashIsEqual(indexHash)) {
|
||||||
System.out.println("Hash problems!!!!!!!");
|
|
||||||
System.out.println(indexHash);
|
|
||||||
System.out.println(indexFileSource.getHash());
|
|
||||||
// TODO: throw exception
|
// 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) {
|
if (manifest.cachedFiles == null) {
|
||||||
manifest.cachedFiles = new HashMap<URI, ManifestFile.File>();
|
manifest.cachedFiles = new HashMap<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: progress bar
|
ui.submitProgress(new InstallProgress("Checking local files..."));
|
||||||
ConcurrentLinkedQueue<Exception> exceptionQueue = new ConcurrentLinkedQueue<Exception>();
|
Iterator<Map.Entry<SpaceSafeURI, ManifestFile.File>> it = manifest.cachedFiles.entrySet().iterator();
|
||||||
List<IndexFile.File> newFiles = indexFile.files.stream().map(f -> {
|
while (it.hasNext()) {
|
||||||
if (f.hashFormat == null || f.hashFormat.length() == 0) {
|
Map.Entry<SpaceSafeURI, ManifestFile.File> entry = it.next();
|
||||||
f.hashFormat = indexFile.hashFormat;
|
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);
|
|
||||||
Object 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
|
if (stateHandler.getCancelButton()) {
|
||||||
// TODO: all options should be presented, not just new files!!!!!!!
|
showCancellationDialog();
|
||||||
// and options should be readded to newFiles after option -> true
|
return;
|
||||||
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);
|
ui.submitProgress(new InstallProgress("Comparing new files..."));
|
||||||
}).forEachOrdered(desc -> {
|
|
||||||
System.out.println(desc);
|
// TODO: progress bar?
|
||||||
|
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.size() > 0) {
|
||||||
|
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?
|
// TODO: different thread pool type?
|
||||||
ExecutorService threadPool = Executors.newFixedThreadPool(10);
|
ExecutorService threadPool = Executors.newFixedThreadPool(10);
|
||||||
CompletionService<DownloadCompletion> completionService = new ExecutorCompletionService<DownloadCompletion>(
|
CompletionService<DownloadTask> completionService = new ExecutorCompletionService<>(threadPool);
|
||||||
threadPool);
|
|
||||||
|
|
||||||
for (IndexFile.File f : newFiles) {
|
tasks.forEach(t -> completionService.submit(() -> {
|
||||||
ManifestFile.File cachedFile = manifest.cachedFiles.get(f.file);
|
t.download(opts.packFolder, indexUri);
|
||||||
completionService.submit(new Callable<DownloadCompletion>() {
|
return t;
|
||||||
public DownloadCompletion call() {
|
}));
|
||||||
DownloadCompletion dc = new DownloadCompletion();
|
|
||||||
dc.file = f;
|
|
||||||
|
|
||||||
if (cachedFile != null && cachedFile.linkedFileHash != null && f.linkedFile != null) {
|
for (int i = 0; i < tasks.size(); i++) {
|
||||||
try {
|
DownloadTask task;
|
||||||
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) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
Object 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;
|
|
||||||
try {
|
try {
|
||||||
ret = completionService.take().get();
|
task = completionService.take().get();
|
||||||
} catch (InterruptedException | ExecutionException e) {
|
} catch (InterruptedException | ExecutionException e) {
|
||||||
// TODO: collect all exceptions, present in one dialog
|
|
||||||
ui.handleException(e);
|
ui.handleException(e);
|
||||||
ret = null;
|
task = null;
|
||||||
}
|
}
|
||||||
// Update manifest
|
// Update manifest - If there were no errors cachedFile has already been modified in place (good old pass by reference)
|
||||||
if (ret != null && ret.err == null && ret.file != null) {
|
if (task != null) {
|
||||||
ManifestFile.File newCachedFile = new ManifestFile.File();
|
if (task.getException() != null) {
|
||||||
try {
|
ManifestFile.File file = task.cachedFile.getRevert();
|
||||||
newCachedFile.hash = ret.file.getHash();
|
if (file != null) {
|
||||||
if (newCachedFile.hash == null) {
|
manifest.cachedFiles.putIfAbsent(task.metadata.file, file);
|
||||||
throw new Exception("Invalid hash!");
|
|
||||||
}
|
}
|
||||||
} 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 {
|
} 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 {
|
} else {
|
||||||
progress = "Failed to download, unknown reason";
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<IExceptionDetails> failedTasks2ElectricBoogaloo = nonFailedFirstTasks.stream().filter(t -> t.getException() != null).collect(Collectors.toList());
|
||||||
|
if (failedTasks2ElectricBoogaloo.size() > 0) {
|
||||||
|
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 {
|
private void showCancellationDialog() {
|
||||||
Exception err;
|
IExceptionDetails.ExceptionListResult exceptionListResult;
|
||||||
IndexFile.File file;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,48 +1,42 @@
|
|||||||
package link.infra.packwiz.installer.metadata;
|
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.google.gson.annotations.SerializedName;
|
||||||
import com.moandjiezana.toml.Toml;
|
import com.moandjiezana.toml.Toml;
|
||||||
|
|
||||||
import link.infra.packwiz.installer.metadata.hash.GeneralHashingSource;
|
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.metadata.hash.HashUtils;
|
||||||
import link.infra.packwiz.installer.request.HandlerManager;
|
import link.infra.packwiz.installer.request.HandlerManager;
|
||||||
import okio.Okio;
|
import okio.Okio;
|
||||||
import okio.Source;
|
import okio.Source;
|
||||||
|
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class IndexFile {
|
public class IndexFile {
|
||||||
@SerializedName("hash-format")
|
@SerializedName("hash-format")
|
||||||
public String hashFormat;
|
public String hashFormat;
|
||||||
public List<File> files;
|
public List<File> files;
|
||||||
|
|
||||||
public static class File {
|
public static class File {
|
||||||
@JsonAdapter(SpaceSafeURIParser.class)
|
public SpaceSafeURI file;
|
||||||
public URI file;
|
|
||||||
@SerializedName("hash-format")
|
@SerializedName("hash-format")
|
||||||
public String hashFormat;
|
public String hashFormat;
|
||||||
public String hash;
|
public String hash;
|
||||||
// TODO: implement
|
public SpaceSafeURI alias;
|
||||||
public String alias;
|
|
||||||
public boolean metafile;
|
public boolean metafile;
|
||||||
// TODO: implement
|
|
||||||
public boolean preserve;
|
public boolean preserve;
|
||||||
|
|
||||||
public transient ModFile linkedFile;
|
public transient ModFile linkedFile;
|
||||||
public transient URI linkedFileURI;
|
public transient SpaceSafeURI linkedFileURI;
|
||||||
public transient boolean optionValue = true;
|
|
||||||
|
|
||||||
public void downloadMeta(IndexFile parentIndexFile, URI indexUri) throws Exception {
|
public void downloadMeta(IndexFile parentIndexFile, SpaceSafeURI indexUri) throws Exception {
|
||||||
if (!metafile) {
|
if (!metafile) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (hashFormat == null || hashFormat.length() == 0) {
|
if (hashFormat == null || hashFormat.length() == 0) {
|
||||||
hashFormat = parentIndexFile.hashFormat;
|
hashFormat = parentIndexFile.hashFormat;
|
||||||
}
|
}
|
||||||
Object fileHash = HashUtils.getHash(hashFormat, hash);
|
Hash fileHash = HashUtils.getHash(hashFormat, hash);
|
||||||
linkedFileURI = HandlerManager.getNewLoc(indexUri, file);
|
linkedFileURI = HandlerManager.getNewLoc(indexUri, file);
|
||||||
Source src = HandlerManager.getFileSource(linkedFileURI);
|
Source src = HandlerManager.getFileSource(linkedFileURI);
|
||||||
GeneralHashingSource fileStream = HashUtils.getHasher(hashFormat).getHashingSource(src);
|
GeneralHashingSource fileStream = HashUtils.getHasher(hashFormat).getHashingSource(src);
|
||||||
@@ -53,14 +47,14 @@ public class IndexFile {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Source getSource(URI indexUri) throws Exception {
|
public Source getSource(SpaceSafeURI indexUri) throws Exception {
|
||||||
if (metafile) {
|
if (metafile) {
|
||||||
if (linkedFile == null) {
|
if (linkedFile == null) {
|
||||||
throw new Exception("Linked file doesn't exist!");
|
throw new Exception("Linked file doesn't exist!");
|
||||||
}
|
}
|
||||||
return linkedFile.getSource(linkedFileURI);
|
return linkedFile.getSource(linkedFileURI);
|
||||||
} else {
|
} else {
|
||||||
URI newLoc = HandlerManager.getNewLoc(indexUri, file);
|
SpaceSafeURI newLoc = HandlerManager.getNewLoc(indexUri, file);
|
||||||
if (newLoc == null) {
|
if (newLoc == null) {
|
||||||
throw new Exception("Index file URI is invalid");
|
throw new Exception("Index file URI is invalid");
|
||||||
}
|
}
|
||||||
@@ -68,8 +62,9 @@ public class IndexFile {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Object getHash() throws Exception {
|
public Hash getHash() throws Exception {
|
||||||
if (hash == null) {
|
if (hash == null) {
|
||||||
|
// TODO: should these be more specific exceptions (e.g. IndexFileException?!)
|
||||||
throw new Exception("Index file doesn't have a hash");
|
throw new Exception("Index file doesn't have a hash");
|
||||||
}
|
}
|
||||||
if (hashFormat == null) {
|
if (hashFormat == null) {
|
||||||
@@ -91,13 +86,17 @@ public class IndexFile {
|
|||||||
if (file != null) {
|
if (file != null) {
|
||||||
return Paths.get(file.getPath()).getFileName().toString();
|
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) {
|
if (metafile && linkedFile != null) {
|
||||||
// TODO: URIs are bad
|
// TODO: URIs are bad
|
||||||
return file.resolve(linkedFile.filename.replace(" ", "%20"));
|
return file.resolve(linkedFile.filename);
|
||||||
} else {
|
} else {
|
||||||
return file;
|
return file;
|
||||||
}
|
}
|
||||||
|
@@ -1,18 +1,39 @@
|
|||||||
package link.infra.packwiz.installer.metadata;
|
package link.infra.packwiz.installer.metadata;
|
||||||
|
|
||||||
import java.net.URI;
|
import link.infra.packwiz.installer.UpdateManager;
|
||||||
|
import link.infra.packwiz.installer.metadata.hash.Hash;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
public class ManifestFile {
|
public class ManifestFile {
|
||||||
|
public Hash packFileHash = null;
|
||||||
public Object packFileHash = null;
|
public Hash indexFileHash = null;
|
||||||
public Object indexFileHash = null;
|
public Map<SpaceSafeURI, File> cachedFiles;
|
||||||
public Map<URI, File> cachedFiles;
|
// If the side changes, EVERYTHING invalidates. FUN!!!
|
||||||
|
public UpdateManager.Options.Side cachedSide = UpdateManager.Options.Side.CLIENT;
|
||||||
|
|
||||||
public static class File {
|
public static class File {
|
||||||
public Object hash = null;
|
private transient File revert;
|
||||||
|
|
||||||
|
public Hash hash = null;
|
||||||
|
public Hash linkedFileHash = null;
|
||||||
|
public String cachedLocation = null;
|
||||||
|
|
||||||
public boolean isOptional = false;
|
public boolean isOptional = false;
|
||||||
public boolean optionValue = true;
|
public boolean optionValue = true;
|
||||||
public Object linkedFileHash = null;
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public File getRevert() {
|
||||||
|
return revert;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,16 +1,14 @@
|
|||||||
package link.infra.packwiz.installer.metadata;
|
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 com.google.gson.annotations.SerializedName;
|
||||||
|
|
||||||
import link.infra.packwiz.installer.UpdateManager.Options.Side;
|
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.metadata.hash.HashUtils;
|
||||||
import link.infra.packwiz.installer.request.HandlerManager;
|
import link.infra.packwiz.installer.request.HandlerManager;
|
||||||
import okio.Source;
|
import okio.Source;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
public class ModFile {
|
public class ModFile {
|
||||||
public String name;
|
public String name;
|
||||||
public String filename;
|
public String filename;
|
||||||
@@ -18,8 +16,7 @@ public class ModFile {
|
|||||||
|
|
||||||
public Download download;
|
public Download download;
|
||||||
public static class Download {
|
public static class Download {
|
||||||
@JsonAdapter(SpaceSafeURIParser.class)
|
public SpaceSafeURI url;
|
||||||
public URI url;
|
|
||||||
@SerializedName("hash-format")
|
@SerializedName("hash-format")
|
||||||
public String hashFormat;
|
public String hashFormat;
|
||||||
public String hash;
|
public String hash;
|
||||||
@@ -35,14 +32,14 @@ public class ModFile {
|
|||||||
public boolean defaultValue;
|
public boolean defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Source getSource(URI baseLoc) throws Exception {
|
public Source getSource(SpaceSafeURI baseLoc) throws Exception {
|
||||||
if (download == null) {
|
if (download == null) {
|
||||||
throw new Exception("Metadata file doesn't have download");
|
throw new Exception("Metadata file doesn't have download");
|
||||||
}
|
}
|
||||||
if (download.url == null) {
|
if (download.url == null) {
|
||||||
throw new Exception("Metadata file doesn't have a download URI");
|
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) {
|
if (newLoc == null) {
|
||||||
throw new Exception("Metadata file URI is invalid");
|
throw new Exception("Metadata file URI is invalid");
|
||||||
}
|
}
|
||||||
@@ -50,7 +47,7 @@ public class ModFile {
|
|||||||
return HandlerManager.getFileSource(newLoc);
|
return HandlerManager.getFileSource(newLoc);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Object getHash() throws Exception {
|
public Hash getHash() throws Exception {
|
||||||
if (download == null) {
|
if (download == null) {
|
||||||
throw new Exception("Metadata file doesn't have download");
|
throw new Exception("Metadata file doesn't have download");
|
||||||
}
|
}
|
||||||
|
@@ -1,18 +1,15 @@
|
|||||||
package link.infra.packwiz.installer.metadata;
|
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 com.google.gson.annotations.SerializedName;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
public class PackFile {
|
public class PackFile {
|
||||||
public String name;
|
public String name;
|
||||||
|
|
||||||
public IndexFileLoc index;
|
public IndexFileLoc index;
|
||||||
public static class IndexFileLoc {
|
public static class IndexFileLoc {
|
||||||
@JsonAdapter(SpaceSafeURIParser.class)
|
public SpaceSafeURI file;
|
||||||
public URI file;
|
|
||||||
@SerializedName("hash-format")
|
@SerializedName("hash-format")
|
||||||
public String hashFormat;
|
public String hashFormat;
|
||||||
public String hash;
|
public String hash;
|
||||||
|
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
@@ -1,26 +1,24 @@
|
|||||||
package link.infra.packwiz.installer.metadata;
|
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.JsonDeserializationContext;
|
||||||
import com.google.gson.JsonDeserializer;
|
import com.google.gson.JsonDeserializer;
|
||||||
import com.google.gson.JsonElement;
|
import com.google.gson.JsonElement;
|
||||||
import com.google.gson.JsonParseException;
|
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
|
* This class encodes spaces before parsing the URI, so the URI can actually be
|
||||||
* parsed.
|
* parsed.
|
||||||
*/
|
*/
|
||||||
class SpaceSafeURIParser implements JsonDeserializer<URI> {
|
class SpaceSafeURIParser implements JsonDeserializer<SpaceSafeURI> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public URI deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
|
public SpaceSafeURI deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
|
||||||
throws JsonParseException {
|
throws JsonParseException {
|
||||||
String uriString = json.getAsString().replace(" ", "%20");
|
|
||||||
try {
|
try {
|
||||||
return new URI(uriString);
|
return new SpaceSafeURI(json.getAsString());
|
||||||
} catch (URISyntaxException e) {
|
} catch (URISyntaxException e) {
|
||||||
throw new JsonParseException("Failed to parse URI", e);
|
throw new JsonParseException("Failed to parse URI", e);
|
||||||
}
|
}
|
||||||
|
@@ -9,7 +9,7 @@ public abstract class GeneralHashingSource extends ForwardingSource {
|
|||||||
super(delegate);
|
super(delegate);
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract Object getHash();
|
public abstract Hash getHash();
|
||||||
|
|
||||||
public boolean hashIsEqual(Object compareTo) {
|
public boolean hashIsEqual(Object compareTo) {
|
||||||
return compareTo.equals(getHash());
|
return compareTo.equals(getHash());
|
||||||
|
@@ -0,0 +1,48 @@
|
|||||||
|
package link.infra.packwiz.installer.metadata.hash;
|
||||||
|
|
||||||
|
import java.lang.reflect.Type;
|
||||||
|
|
||||||
|
import com.google.gson.JsonDeserializationContext;
|
||||||
|
import com.google.gson.JsonDeserializer;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParseException;
|
||||||
|
import com.google.gson.JsonPrimitive;
|
||||||
|
import com.google.gson.JsonSerializationContext;
|
||||||
|
import com.google.gson.JsonSerializer;
|
||||||
|
|
||||||
|
public abstract class Hash {
|
||||||
|
protected abstract String getStringValue();
|
||||||
|
|
||||||
|
protected abstract String getType();
|
||||||
|
|
||||||
|
public static class TypeHandler implements JsonDeserializer<Hash>, JsonSerializer<Hash> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JsonElement serialize(Hash src, Type typeOfSrc, JsonSerializationContext context) {
|
||||||
|
JsonObject out = new JsonObject();
|
||||||
|
out.add("type", new JsonPrimitive(src.getType()));
|
||||||
|
out.add("value", new JsonPrimitive(src.getStringValue()));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Hash deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
|
||||||
|
throws JsonParseException {
|
||||||
|
JsonObject obj = json.getAsJsonObject();
|
||||||
|
String type, value;
|
||||||
|
try {
|
||||||
|
type = obj.get("type").getAsString();
|
||||||
|
value = obj.get("value").getAsString();
|
||||||
|
} catch (NullPointerException e) {
|
||||||
|
throw new JsonParseException("Invalid hash JSON data");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return HashUtils.getHash(type, value);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new JsonParseException("Failed to create hash object", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@@ -18,7 +18,7 @@ public class HashUtils {
|
|||||||
return hasher;
|
return hasher;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Object getHash(String type, String value) throws Exception {
|
public static Hash getHash(String type, String value) throws Exception {
|
||||||
if (hashTypeConversion.containsKey(type)) {
|
if (hashTypeConversion.containsKey(type)) {
|
||||||
return hashTypeConversion.get(type).getHash(value);
|
return hashTypeConversion.get(type).getHash(value);
|
||||||
}
|
}
|
||||||
|
@@ -21,7 +21,7 @@ public class HashingSourceHasher implements IHasher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object getHash() {
|
public Hash getHash() {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
value = new HashingSourceHash(delegateHashing.hash().hex());
|
value = new HashingSourceHash(delegateHashing.hash().hex());
|
||||||
}
|
}
|
||||||
@@ -33,7 +33,7 @@ public class HashingSourceHasher implements IHasher {
|
|||||||
// this some funky inner class stuff
|
// this some funky inner class stuff
|
||||||
// each of these classes is specific to the instance of the HasherHashingSource
|
// each of these classes is specific to the instance of the HasherHashingSource
|
||||||
// therefore HashingSourceHashes from different parent instances will be not instanceof each other
|
// therefore HashingSourceHashes from different parent instances will be not instanceof each other
|
||||||
private class HashingSourceHash {
|
private class HashingSourceHash extends Hash {
|
||||||
String value;
|
String value;
|
||||||
private HashingSourceHash(String value) {
|
private HashingSourceHash(String value) {
|
||||||
this.value = value;
|
this.value = value;
|
||||||
@@ -48,7 +48,7 @@ public class HashingSourceHasher implements IHasher {
|
|||||||
if (value != null) {
|
if (value != null) {
|
||||||
return value.equals(objHash.value);
|
return value.equals(objHash.value);
|
||||||
} else {
|
} else {
|
||||||
return objHash.value == null ? true : false;
|
return objHash.value == null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,6 +56,16 @@ public class HashingSourceHasher implements IHasher {
|
|||||||
public String toString() {
|
public String toString() {
|
||||||
return type + ": " + value;
|
return type + ": " + value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getStringValue() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -69,7 +79,7 @@ public class HashingSourceHasher implements IHasher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object getHash(String value) {
|
public Hash getHash(String value) {
|
||||||
return new HashingSourceHash(value);
|
return new HashingSourceHash(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -4,5 +4,5 @@ import okio.Source;
|
|||||||
|
|
||||||
public interface IHasher {
|
public interface IHasher {
|
||||||
public GeneralHashingSource getHashingSource(Source delegate);
|
public GeneralHashingSource getHashingSource(Source delegate);
|
||||||
public Object getHash(String value);
|
public Hash getHash(String value);
|
||||||
}
|
}
|
@@ -1,10 +1,10 @@
|
|||||||
package link.infra.packwiz.installer.metadata.hash;
|
package link.infra.packwiz.installer.metadata.hash;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
import okio.Buffer;
|
import okio.Buffer;
|
||||||
import okio.Source;
|
import okio.Source;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
public class Murmur2Hasher implements IHasher {
|
public class Murmur2Hasher implements IHasher {
|
||||||
private class Murmur2GeneralHashingSource extends GeneralHashingSource {
|
private class Murmur2GeneralHashingSource extends GeneralHashingSource {
|
||||||
Murmur2Hash value;
|
Murmur2Hash value;
|
||||||
@@ -28,7 +28,7 @@ public class Murmur2Hasher implements IHasher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object getHash() {
|
public Hash getHash() {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
byte[] data = computeNormalizedArray(internalBuffer.readByteArray());
|
byte[] data = computeNormalizedArray(internalBuffer.readByteArray());
|
||||||
value = new Murmur2Hash(Murmur2Lib.hash32(data, data.length, 1));
|
value = new Murmur2Hash(Murmur2Lib.hash32(data, data.length, 1));
|
||||||
@@ -40,8 +40,7 @@ public class Murmur2Hasher implements IHasher {
|
|||||||
private byte[] computeNormalizedArray(byte[] input) {
|
private byte[] computeNormalizedArray(byte[] input) {
|
||||||
byte[] output = new byte[input.length];
|
byte[] output = new byte[input.length];
|
||||||
int num = 0;
|
int num = 0;
|
||||||
for (int i = 0; i < input.length; i++) {
|
for (byte b : input) {
|
||||||
byte b = input[i];
|
|
||||||
if (!(b == 9 || b == 10 || b == 13 || b == 32)) {
|
if (!(b == 9 || b == 10 || b == 13 || b == 32)) {
|
||||||
output[num] = b;
|
output[num] = b;
|
||||||
num++;
|
num++;
|
||||||
@@ -54,7 +53,7 @@ public class Murmur2Hasher implements IHasher {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private class Murmur2Hash {
|
private static class Murmur2Hash extends Hash {
|
||||||
int value;
|
int value;
|
||||||
private Murmur2Hash(String value) {
|
private Murmur2Hash(String value) {
|
||||||
// Parsing as long then casting to int converts values gt int max value but lt uint max value
|
// Parsing as long then casting to int converts values gt int max value but lt uint max value
|
||||||
@@ -79,6 +78,16 @@ public class Murmur2Hasher implements IHasher {
|
|||||||
public String toString() {
|
public String toString() {
|
||||||
return "murmur2: " + value;
|
return "murmur2: " + value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getStringValue() {
|
||||||
|
return Integer.toString(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getType() {
|
||||||
|
return "murmur2";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -87,7 +96,7 @@ public class Murmur2Hasher implements IHasher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object getHash(String value) {
|
public Hash getHash(String value) {
|
||||||
return new Murmur2Hash(value);
|
return new Murmur2Hash(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,23 +1,23 @@
|
|||||||
package link.infra.packwiz.installer.request;
|
package link.infra.packwiz.installer.request;
|
||||||
|
|
||||||
import java.net.URI;
|
import link.infra.packwiz.installer.metadata.SpaceSafeURI;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import link.infra.packwiz.installer.request.handlers.RequestHandlerGithub;
|
import link.infra.packwiz.installer.request.handlers.RequestHandlerGithub;
|
||||||
import link.infra.packwiz.installer.request.handlers.RequestHandlerHTTP;
|
import link.infra.packwiz.installer.request.handlers.RequestHandlerHTTP;
|
||||||
import okio.Source;
|
import okio.Source;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public abstract class HandlerManager {
|
public abstract class HandlerManager {
|
||||||
|
|
||||||
public static List<IRequestHandler> handlers = new ArrayList<IRequestHandler>();
|
private static List<IRequestHandler> handlers = new ArrayList<>();
|
||||||
|
|
||||||
static {
|
static {
|
||||||
handlers.add(new RequestHandlerGithub());
|
handlers.add(new RequestHandlerGithub());
|
||||||
handlers.add(new RequestHandlerHTTP());
|
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 (loc == null) return null;
|
||||||
if (base != null) {
|
if (base != null) {
|
||||||
loc = base.resolve(loc);
|
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
|
// Zip handler discards once read, requesting multiple times on other handlers would cause multiple downloads
|
||||||
// Caching system? Copy from already downloaded files?
|
// 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) {
|
for (IRequestHandler handler : handlers) {
|
||||||
if (handler.matchesHandler(loc)) {
|
if (handler.matchesHandler(loc)) {
|
||||||
Source src = handler.getFileSource(loc);
|
Source src = handler.getFileSource(loc);
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
package link.infra.packwiz.installer.request;
|
package link.infra.packwiz.installer.request;
|
||||||
|
|
||||||
import java.net.URI;
|
import link.infra.packwiz.installer.metadata.SpaceSafeURI;
|
||||||
|
|
||||||
import okio.Source;
|
import okio.Source;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -9,9 +8,9 @@ import okio.Source;
|
|||||||
*/
|
*/
|
||||||
public interface IRequestHandler {
|
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;
|
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.
|
* It is assumed that each location is read only once for the duration of an IRequestHandler.
|
||||||
* @param loc The location to be read
|
* @param loc The location to be read
|
||||||
* @return The Source containing the data of the file
|
* @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;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
package link.infra.packwiz.installer.request.handlers;
|
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.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||||
@@ -14,38 +15,41 @@ public class RequestHandlerGithub extends RequestHandlerZip {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public URI getNewLoc(URI loc) {
|
public SpaceSafeURI getNewLoc(SpaceSafeURI loc) {
|
||||||
return loc;
|
return loc;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: is caching really needed, if HTTPURLConnection follows redirects correctly?
|
// TODO: is caching really needed, if HTTPURLConnection follows redirects correctly?
|
||||||
private Map<String, URI> zipUriMap = new HashMap<String, URI>();
|
private Map<String, SpaceSafeURI> zipUriMap = new HashMap<>();
|
||||||
final ReentrantReadWriteLock zipUriLock = new ReentrantReadWriteLock();
|
private final ReentrantReadWriteLock zipUriLock = new ReentrantReadWriteLock();
|
||||||
private static Pattern repoMatcherPattern = Pattern.compile("/([\\w.-]+/[\\w.-]+).*");
|
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 matcher = repoMatcherPattern.matcher(loc.getPath());
|
||||||
matcher.matches();
|
if (matcher.matches()) {
|
||||||
return matcher.group(1);
|
return matcher.group(1);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected URI getZipUri(URI loc) throws Exception {
|
protected SpaceSafeURI getZipUri(SpaceSafeURI loc) throws Exception {
|
||||||
String repoName = getRepoName(loc);
|
String repoName = getRepoName(loc);
|
||||||
String branchName = getBranch(loc);
|
String branchName = getBranch(loc);
|
||||||
zipUriLock.readLock().lock();
|
zipUriLock.readLock().lock();
|
||||||
URI zipUri = zipUriMap.get(repoName + "/" + branchName);
|
SpaceSafeURI zipUri = zipUriMap.get(repoName + "/" + branchName);
|
||||||
zipUriLock.readLock().unlock();
|
zipUriLock.readLock().unlock();
|
||||||
if (zipUri != null) {
|
if (zipUri != null) {
|
||||||
return zipUri;
|
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();
|
zipUriLock.writeLock().lock();
|
||||||
// If another thread sets the value concurrently, use the value of the
|
// If another thread sets the value concurrently, use the value of the
|
||||||
// thread that first acquired the lock.
|
// thread that first acquired the lock.
|
||||||
URI zipUriInserted = zipUriMap.putIfAbsent(repoName + "/" + branchName, zipUri);
|
SpaceSafeURI zipUriInserted = zipUriMap.putIfAbsent(repoName + "/" + branchName, zipUri);
|
||||||
if (zipUriInserted != null) {
|
if (zipUriInserted != null) {
|
||||||
zipUri = zipUriInserted;
|
zipUri = zipUriInserted;
|
||||||
}
|
}
|
||||||
@@ -55,20 +59,23 @@ public class RequestHandlerGithub extends RequestHandlerZip {
|
|||||||
|
|
||||||
private static Pattern branchMatcherPattern = Pattern.compile("/[\\w.-]+/[\\w.-]+/blob/([\\w.-]+).*");
|
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 matcher = branchMatcherPattern.matcher(loc.getPath());
|
||||||
matcher.matches();
|
if (matcher.matches()) {
|
||||||
return matcher.group(1);
|
return matcher.group(1);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected URI getLocationInZip(URI loc) throws Exception {
|
protected SpaceSafeURI getLocationInZip(SpaceSafeURI loc) throws Exception {
|
||||||
String path = "/" + getRepoName(loc) + "/blob/" + getBranch(loc);
|
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
|
@Override
|
||||||
public boolean matchesHandler(URI loc) {
|
public boolean matchesHandler(SpaceSafeURI loc) {
|
||||||
String scheme = loc.getScheme();
|
String scheme = loc.getScheme();
|
||||||
if (!("http".equals(scheme) || "https".equals(scheme))) {
|
if (!("http".equals(scheme) || "https".equals(scheme))) {
|
||||||
return false;
|
return false;
|
||||||
|
@@ -1,22 +1,22 @@
|
|||||||
package link.infra.packwiz.installer.request.handlers;
|
package link.infra.packwiz.installer.request.handlers;
|
||||||
|
|
||||||
import java.net.URI;
|
import link.infra.packwiz.installer.metadata.SpaceSafeURI;
|
||||||
import java.net.URLConnection;
|
|
||||||
|
|
||||||
import link.infra.packwiz.installer.request.IRequestHandler;
|
import link.infra.packwiz.installer.request.IRequestHandler;
|
||||||
import okio.Okio;
|
import okio.Okio;
|
||||||
import okio.Source;
|
import okio.Source;
|
||||||
|
|
||||||
|
import java.net.URLConnection;
|
||||||
|
|
||||||
public class RequestHandlerHTTP implements IRequestHandler {
|
public class RequestHandlerHTTP implements IRequestHandler {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean matchesHandler(URI loc) {
|
public boolean matchesHandler(SpaceSafeURI loc) {
|
||||||
String scheme = loc.getScheme();
|
String scheme = loc.getScheme();
|
||||||
return "http".equals(scheme) || "https".equals(scheme);
|
return "http".equals(scheme) || "https".equals(scheme);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Source getFileSource(URI loc) throws Exception {
|
public Source getFileSource(SpaceSafeURI loc) throws Exception {
|
||||||
URLConnection conn = loc.toURL().openConnection();
|
URLConnection conn = loc.toURL().openConnection();
|
||||||
// TODO: when do we send specific headers??? should there be a way to signal this?
|
// TODO: when do we send specific headers??? should there be a way to signal this?
|
||||||
// github *sometimes* requires it, sometimes not!
|
// github *sometimes* requires it, sometimes not!
|
||||||
|
@@ -1,7 +1,12 @@
|
|||||||
package link.infra.packwiz.installer.request.handlers;
|
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.io.IOException;
|
||||||
import java.net.URI;
|
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -11,14 +16,9 @@ import java.util.function.Predicate;
|
|||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipInputStream;
|
import java.util.zip.ZipInputStream;
|
||||||
|
|
||||||
import okio.Buffer;
|
|
||||||
import okio.BufferedSource;
|
|
||||||
import okio.Okio;
|
|
||||||
import okio.Source;
|
|
||||||
|
|
||||||
public abstract class RequestHandlerZip extends RequestHandlerHTTP {
|
public abstract class RequestHandlerZip extends RequestHandlerHTTP {
|
||||||
|
|
||||||
protected final boolean modeHasFolder;
|
private final boolean modeHasFolder;
|
||||||
|
|
||||||
public RequestHandlerZip(boolean modeHasFolder) {
|
public RequestHandlerZip(boolean modeHasFolder) {
|
||||||
this.modeHasFolder = modeHasFolder;
|
this.modeHasFolder = modeHasFolder;
|
||||||
@@ -35,14 +35,14 @@ public abstract class RequestHandlerZip extends RequestHandlerHTTP {
|
|||||||
private class ZipReader {
|
private class ZipReader {
|
||||||
|
|
||||||
private final ZipInputStream zis;
|
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!
|
// Write lock implies access to ZipInputStream - only 1 thread must read at a time!
|
||||||
final ReentrantLock filesLock = new ReentrantLock();
|
final ReentrantLock filesLock = new ReentrantLock();
|
||||||
private ZipEntry entry;
|
private ZipEntry entry;
|
||||||
|
|
||||||
private final BufferedSource zipSource;
|
private final BufferedSource zipSource;
|
||||||
|
|
||||||
public ZipReader(Source zip) {
|
ZipReader(Source zip) {
|
||||||
zis = new ZipInputStream(Okio.buffer(zip).inputStream());
|
zis = new ZipInputStream(Okio.buffer(zip).inputStream());
|
||||||
zipSource = Okio.buffer(Okio.source(zis));
|
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
|
// 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) {
|
while (true) {
|
||||||
entry = zis.getNextEntry();
|
entry = zis.getNextEntry();
|
||||||
if (entry == null) {
|
if (entry == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
Buffer data = readCurrFile();
|
Buffer data = readCurrFile();
|
||||||
URI fileLoc = new URI(removeFolder(entry.getName()));
|
SpaceSafeURI fileLoc = new SpaceSafeURI(removeFolder(entry.getName()));
|
||||||
if (loc.equals(fileLoc)) {
|
if (loc.equals(fileLoc)) {
|
||||||
return data;
|
return data;
|
||||||
} else {
|
} 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();
|
filesLock.lock();
|
||||||
// Assume files are only read once, allow GC by removing
|
// Assume files are only read once, allow GC by removing
|
||||||
Buffer file = readFiles.remove(loc);
|
Buffer file = readFiles.remove(loc);
|
||||||
@@ -82,15 +82,12 @@ public abstract class RequestHandlerZip extends RequestHandlerHTTP {
|
|||||||
|
|
||||||
file = findFile(loc);
|
file = findFile(loc);
|
||||||
filesLock.unlock();
|
filesLock.unlock();
|
||||||
if (file != null) {
|
return file;
|
||||||
return file;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public URI findInZip(Predicate<URI> matches) throws Exception {
|
SpaceSafeURI findInZip(Predicate<SpaceSafeURI> matches) throws Exception {
|
||||||
filesLock.lock();
|
filesLock.lock();
|
||||||
for (URI file : readFiles.keySet()) {
|
for (SpaceSafeURI file : readFiles.keySet()) {
|
||||||
if (matches.test(file)) {
|
if (matches.test(file)) {
|
||||||
filesLock.unlock();
|
filesLock.unlock();
|
||||||
return file;
|
return file;
|
||||||
@@ -104,7 +101,7 @@ public abstract class RequestHandlerZip extends RequestHandlerHTTP {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
Buffer data = readCurrFile();
|
Buffer data = readCurrFile();
|
||||||
URI fileLoc = new URI(removeFolder(entry.getName()));
|
SpaceSafeURI fileLoc = new SpaceSafeURI(removeFolder(entry.getName()));
|
||||||
readFiles.put(fileLoc, data);
|
readFiles.put(fileLoc, data);
|
||||||
if (matches.test(fileLoc)) {
|
if (matches.test(fileLoc)) {
|
||||||
filesLock.unlock();
|
filesLock.unlock();
|
||||||
@@ -115,19 +112,19 @@ public abstract class RequestHandlerZip extends RequestHandlerHTTP {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private final Map<URI, ZipReader> cache = new HashMap<URI, ZipReader>();
|
private final Map<SpaceSafeURI, ZipReader> cache = new HashMap<>();
|
||||||
final ReentrantReadWriteLock cacheLock = new ReentrantReadWriteLock();
|
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
|
@Override
|
||||||
public abstract boolean matchesHandler(URI loc);
|
public abstract boolean matchesHandler(SpaceSafeURI loc);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Source getFileSource(URI loc) throws Exception {
|
public Source getFileSource(SpaceSafeURI loc) throws Exception {
|
||||||
URI zipUri = getZipUri(loc);
|
SpaceSafeURI zipUri = getZipUri(loc);
|
||||||
cacheLock.readLock().lock();
|
cacheLock.readLock().lock();
|
||||||
ZipReader zr = cache.get(zipUri);
|
ZipReader zr = cache.get(zipUri);
|
||||||
cacheLock.readLock().unlock();
|
cacheLock.readLock().unlock();
|
||||||
@@ -150,8 +147,8 @@ public abstract class RequestHandlerZip extends RequestHandlerHTTP {
|
|||||||
return zr.getFileSource(getLocationInZip(loc));
|
return zr.getFileSource(getLocationInZip(loc));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected URI findInZip(URI loc, Predicate<URI> matches) throws Exception {
|
protected SpaceSafeURI findInZip(SpaceSafeURI loc, Predicate<SpaceSafeURI> matches) throws Exception {
|
||||||
URI zipUri = getZipUri(loc);
|
SpaceSafeURI zipUri = getZipUri(loc);
|
||||||
cacheLock.readLock().lock();
|
cacheLock.readLock().lock();
|
||||||
ZipReader zr = cache.get(zipUri);
|
ZipReader zr = cache.get(zipUri);
|
||||||
cacheLock.readLock().unlock();
|
cacheLock.readLock().unlock();
|
||||||
|
@@ -1,5 +1,9 @@
|
|||||||
package link.infra.packwiz.installer.ui;
|
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 {
|
public class CLIHandler implements IUserInterface {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -8,7 +12,7 @@ public class CLIHandler implements IUserInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void show() {}
|
public void show(InputStateHandler h) {}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void submitProgress(InstallProgress progress) {
|
public void submitProgress(InstallProgress progress) {
|
||||||
@@ -27,6 +31,19 @@ public class CLIHandler implements IUserInterface {
|
|||||||
@Override
|
@Override
|
||||||
public void executeManager(Runnable task) {
|
public void executeManager(Runnable task) {
|
||||||
task.run();
|
task.run();
|
||||||
|
System.out.println("Finished successfully!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Future<Boolean> showOptions(List<IOptionDetails> option) {
|
||||||
|
throw new RuntimeException("Optional mods not implemented for CLI! Make sure your optional mods are only on the client side!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,10 @@
|
|||||||
|
package link.infra.packwiz.installer.ui;
|
||||||
|
|
||||||
|
public interface IExceptionDetails {
|
||||||
|
Exception getException();
|
||||||
|
String getName();
|
||||||
|
|
||||||
|
enum ExceptionListResult {
|
||||||
|
CONTINUE, CANCEL, IGNORE
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,8 @@
|
|||||||
|
package link.infra.packwiz.installer.ui;
|
||||||
|
|
||||||
|
public interface IOptionDetails {
|
||||||
|
String getName();
|
||||||
|
boolean getOptionValue();
|
||||||
|
String getOptionDescription();
|
||||||
|
void setOptionValue(boolean value);
|
||||||
|
}
|
@@ -1,23 +1,38 @@
|
|||||||
package link.infra.packwiz.installer.ui;
|
package link.infra.packwiz.installer.ui;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
|
||||||
public interface IUserInterface {
|
public interface IUserInterface {
|
||||||
|
|
||||||
public void show();
|
void show(InputStateHandler handler);
|
||||||
|
|
||||||
public void handleException(Exception e);
|
void handleException(Exception e);
|
||||||
|
|
||||||
/**
|
default void handleExceptionAndExit(Exception e) {
|
||||||
* This might not exit straight away, return after calling this!
|
|
||||||
*/
|
|
||||||
public default void handleExceptionAndExit(Exception e) {
|
|
||||||
handleException(e);
|
handleException(e);
|
||||||
System.exit(1);
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
@@ -1,51 +1,42 @@
|
|||||||
package link.infra.packwiz.installer.ui;
|
package link.infra.packwiz.installer.ui;
|
||||||
|
|
||||||
import java.awt.BorderLayout;
|
import javax.swing.*;
|
||||||
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.border.EmptyBorder;
|
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 {
|
public class InstallWindow implements IUserInterface {
|
||||||
|
|
||||||
private JFrame frmPackwizlauncher;
|
private JFrame frmPackwizlauncher;
|
||||||
private JLabel lblProgresslabel;
|
private JLabel lblProgresslabel;
|
||||||
private JProgressBar progressBar;
|
private JProgressBar progressBar;
|
||||||
|
private InputStateHandler inputStateHandler;
|
||||||
|
|
||||||
private String title = "Updating modpack...";
|
private String title = "Updating modpack...";
|
||||||
private SwingWorkerButWithPublicPublish<Void, InstallProgress> worker;
|
private SwingWorkerButWithPublicPublish<Void, InstallProgress> worker;
|
||||||
private AtomicBoolean aboutToCrash = new AtomicBoolean();
|
private AtomicBoolean aboutToCrash = new AtomicBoolean();
|
||||||
|
private JButton btnOptions;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void show() {
|
public void show(InputStateHandler handler) {
|
||||||
EventQueue.invokeLater(new Runnable() {
|
this.inputStateHandler = handler;
|
||||||
public void run() {
|
EventQueue.invokeLater(() -> {
|
||||||
try {
|
try {
|
||||||
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
|
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
|
||||||
InstallWindow.this.initialize();
|
initialize();
|
||||||
InstallWindow.this.frmPackwizlauncher.setVisible(true);
|
frmPackwizlauncher.setVisible(true);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the contents of the frame.
|
* Initialize the contents of the frame.
|
||||||
|
* @wbp.parser.entryPoint
|
||||||
*/
|
*/
|
||||||
private void initialize() {
|
private void initialize() {
|
||||||
frmPackwizlauncher = new JFrame();
|
frmPackwizlauncher = new JFrame();
|
||||||
@@ -53,43 +44,41 @@ public class InstallWindow implements IUserInterface {
|
|||||||
frmPackwizlauncher.setBounds(100, 100, 493, 95);
|
frmPackwizlauncher.setBounds(100, 100, 493, 95);
|
||||||
frmPackwizlauncher.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
frmPackwizlauncher.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
||||||
frmPackwizlauncher.setLocationRelativeTo(null);
|
frmPackwizlauncher.setLocationRelativeTo(null);
|
||||||
|
|
||||||
JPanel panel = new JPanel();
|
JPanel panel = new JPanel();
|
||||||
panel.setBorder(new EmptyBorder(10, 10, 10, 10));
|
panel.setBorder(new EmptyBorder(10, 10, 10, 10));
|
||||||
frmPackwizlauncher.getContentPane().add(panel, BorderLayout.CENTER);
|
frmPackwizlauncher.getContentPane().add(panel, BorderLayout.CENTER);
|
||||||
panel.setLayout(new BorderLayout(0, 0));
|
panel.setLayout(new BorderLayout(0, 0));
|
||||||
|
|
||||||
progressBar = new JProgressBar();
|
progressBar = new JProgressBar();
|
||||||
progressBar.setIndeterminate(true);
|
progressBar.setIndeterminate(true);
|
||||||
panel.add(progressBar, BorderLayout.CENTER);
|
panel.add(progressBar, BorderLayout.CENTER);
|
||||||
|
|
||||||
lblProgresslabel = new JLabel("Loading...");
|
lblProgresslabel = new JLabel("Loading...");
|
||||||
panel.add(lblProgresslabel, BorderLayout.SOUTH);
|
panel.add(lblProgresslabel, BorderLayout.SOUTH);
|
||||||
|
|
||||||
JPanel panel_1 = new JPanel();
|
JPanel panel_1 = new JPanel();
|
||||||
panel_1.setBorder(new EmptyBorder(0, 5, 0, 5));
|
panel_1.setBorder(new EmptyBorder(0, 5, 0, 5));
|
||||||
frmPackwizlauncher.getContentPane().add(panel_1, BorderLayout.EAST);
|
frmPackwizlauncher.getContentPane().add(panel_1, BorderLayout.EAST);
|
||||||
GridBagLayout gbl_panel_1 = new GridBagLayout();
|
GridBagLayout gbl_panel_1 = new GridBagLayout();
|
||||||
panel_1.setLayout(gbl_panel_1);
|
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);
|
btnOptions.setAlignmentX(Component.CENTER_ALIGNMENT);
|
||||||
GridBagConstraints gbc_btnOptions = new GridBagConstraints();
|
GridBagConstraints gbc_btnOptions = new GridBagConstraints();
|
||||||
gbc_btnOptions.gridx = 0;
|
gbc_btnOptions.gridx = 0;
|
||||||
gbc_btnOptions.gridy = 0;
|
gbc_btnOptions.gridy = 0;
|
||||||
panel_1.add(btnOptions, gbc_btnOptions);
|
panel_1.add(btnOptions, gbc_btnOptions);
|
||||||
|
|
||||||
JButton btnCancel = new JButton("Cancel");
|
JButton btnCancel = new JButton("Cancel");
|
||||||
btnCancel.addActionListener(new ActionListener() {
|
btnCancel.addActionListener(e -> {
|
||||||
public void actionPerformed(ActionEvent arg0) {
|
btnCancel.setEnabled(false);
|
||||||
if (worker != null) {
|
inputStateHandler.pressCancelButton();
|
||||||
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.setAlignmentX(Component.CENTER_ALIGNMENT);
|
btnCancel.setAlignmentX(Component.CENTER_ALIGNMENT);
|
||||||
GridBagConstraints gbc_btnCancel = new GridBagConstraints();
|
GridBagConstraints gbc_btnCancel = new GridBagConstraints();
|
||||||
@@ -101,10 +90,8 @@ public class InstallWindow implements IUserInterface {
|
|||||||
@Override
|
@Override
|
||||||
public void handleException(Exception e) {
|
public void handleException(Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
EventQueue.invokeLater(new Runnable() {
|
EventQueue.invokeLater(() -> {
|
||||||
public void run() {
|
JOptionPane.showMessageDialog(null, "An error occurred: \n" + e.getClass().getCanonicalName() + ": " + e.getMessage(), title, JOptionPane.ERROR_MESSAGE);
|
||||||
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();
|
e.printStackTrace();
|
||||||
// Used to prevent the done() handler of SwingWorker executing if the invokeLater hasn't happened yet
|
// Used to prevent the done() handler of SwingWorker executing if the invokeLater hasn't happened yet
|
||||||
aboutToCrash.set(true);
|
aboutToCrash.set(true);
|
||||||
EventQueue.invokeLater(new Runnable() {
|
EventQueue.invokeLater(() -> {
|
||||||
public void run() {
|
JOptionPane.showMessageDialog(null, "A fatal error occurred: \n" + e.getClass().getCanonicalName() + ": " + e.getMessage(), title, JOptionPane.ERROR_MESSAGE);
|
||||||
JOptionPane.showMessageDialog(null, "A fatal error occurred: \n" + e.getClass().getCanonicalName() + ": " + e.getMessage(), title, JOptionPane.ERROR_MESSAGE);
|
System.exit(1);
|
||||||
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
|
@Override
|
||||||
public void setTitle(String title) {
|
public void setTitle(String title) {
|
||||||
this.title = title;
|
this.title = title;
|
||||||
if (frmPackwizlauncher != null) {
|
if (frmPackwizlauncher != null) {
|
||||||
EventQueue.invokeLater(new Runnable() {
|
EventQueue.invokeLater(() -> frmPackwizlauncher.setTitle(title));
|
||||||
public void run() {
|
|
||||||
InstallWindow.this.frmPackwizlauncher.setTitle(title);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void submitProgress(InstallProgress progress) {
|
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) {
|
if (worker != null) {
|
||||||
worker.publishPublic(progress);
|
worker.publishPublic(progress);
|
||||||
}
|
}
|
||||||
@@ -142,48 +140,89 @@ public class InstallWindow implements IUserInterface {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void executeManager(Runnable task) {
|
public void executeManager(Runnable task) {
|
||||||
EventQueue.invokeLater(new Runnable() {
|
EventQueue.invokeLater(() -> {
|
||||||
public void run() {
|
worker = new SwingWorkerButWithPublicPublish<Void, InstallProgress>() {
|
||||||
worker = new SwingWorkerButWithPublicPublish<Void, InstallProgress>() {
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Void doInBackground() throws Exception {
|
protected Void doInBackground() {
|
||||||
task.run();
|
task.run();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void process(List<InstallProgress> chunks) {
|
protected void process(List<InstallProgress> chunks) {
|
||||||
// Only process last chunk
|
// Only process last chunk
|
||||||
if (chunks.size() > 0) {
|
if (chunks.size() > 0) {
|
||||||
InstallProgress prog = chunks.get(chunks.size() - 1);
|
InstallProgress prog = chunks.get(chunks.size() - 1);
|
||||||
if (prog.hasProgress) {
|
if (prog.hasProgress) {
|
||||||
progressBar.setIndeterminate(false);
|
progressBar.setIndeterminate(false);
|
||||||
progressBar.setValue(prog.progress);
|
progressBar.setValue(prog.progress);
|
||||||
progressBar.setMaximum(prog.progressTotal);
|
progressBar.setMaximum(prog.progressTotal);
|
||||||
} else {
|
} else {
|
||||||
progressBar.setIndeterminate(true);
|
progressBar.setIndeterminate(true);
|
||||||
progressBar.setValue(0);
|
progressBar.setValue(0);
|
||||||
}
|
|
||||||
lblProgresslabel.setText(prog.message);
|
|
||||||
}
|
}
|
||||||
|
lblProgresslabel.setText(prog.message);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void done() {
|
protected void done() {
|
||||||
if (aboutToCrash.get()) {
|
if (aboutToCrash.get()) {
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
// TODO: a better way to do this?
|
|
||||||
frmPackwizlauncher.dispose();
|
|
||||||
System.out.println("Finished successfully!");
|
|
||||||
System.exit(0);
|
|
||||||
}
|
}
|
||||||
|
// TODO: a better way to do this?
|
||||||
};
|
frmPackwizlauncher.dispose();
|
||||||
worker.execute();
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Reference in New Issue
Block a user