Add the base files to the Launcher Git.

This commit is contained in:
Charles Descamps 2024-12-12 10:36:18 -07:00
parent d1b3d2168c
commit 7242015c76
376 changed files with 27499 additions and 2 deletions

View file

@ -0,0 +1,219 @@
/*
* SK's Minecraft Launcher
* Copyright (C) 2010-2014 Albert Pham <http://www.sk89q.com> and contributors
* Please see LICENSE.txt for license information.
*/
package com.skcraft.launcher;
import com.skcraft.launcher.bootstrap.*;
import lombok.Getter;
import lombok.extern.java.Log;
import javax.swing.*;
import javax.swing.filechooser.FileSystemView;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.*;
import java.util.logging.Level;
import static com.skcraft.launcher.bootstrap.SharedLocale.tr;
@Log
public class Bootstrap {
private static final int BOOTSTRAP_VERSION = 1;
@Getter private final File baseDir;
@Getter private final boolean portable;
@Getter private final File binariesDir;
@Getter private final Properties properties;
private final String[] originalArgs;
public static void main(String[] args) throws Throwable {
SimpleLogFormatter.configureGlobalLogger();
SharedLocale.loadBundle("com.skcraft.launcher.lang.Bootstrap", Locale.getDefault());
boolean portable = isPortableMode();
Bootstrap bootstrap = new Bootstrap(portable, args);
try {
bootstrap.cleanup();
bootstrap.launch();
} catch (Throwable t) {
Bootstrap.log.log(Level.WARNING, "Error", t);
Bootstrap.setSwingLookAndFeel();
SwingHelper.showErrorDialog(null, tr("errors.bootstrapError"), tr("errorTitle"), t);
}
}
public Bootstrap(boolean portable, String[] args) throws IOException {
this.properties = BootstrapUtils.loadProperties(Bootstrap.class, "bootstrap.properties");
File baseDir = portable ? new File(".") : getUserLauncherDir();
this.baseDir = baseDir;
this.portable = portable;
this.binariesDir = new File(baseDir, "launcher");
this.originalArgs = args;
binariesDir.mkdirs();
}
public void cleanup() {
File[] files = binariesDir.listFiles(new FileFilter() {
@Override
public boolean accept(File pathname) {
return pathname.getName().endsWith(".tmp");
}
});
if (files != null) {
for (File file : files) {
file.delete();
}
}
}
public void launch() throws Throwable {
File[] files = binariesDir.listFiles(new LauncherBinary.Filter());
List<LauncherBinary> binaries = new ArrayList<LauncherBinary>();
if (files != null) {
for (File file : files) {
Bootstrap.log.info("Found " + file.getAbsolutePath() + "...");
binaries.add(new LauncherBinary(file));
}
}
if (!binaries.isEmpty()) {
launchExisting(binaries, true);
} else {
launchInitial();
}
}
public void launchInitial() throws Exception {
Bootstrap.log.info("Downloading the launcher...");
Thread thread = new Thread(new Downloader(this));
thread.start();
}
public void launchExisting(List<LauncherBinary> binaries, boolean redownload) throws Exception {
Collections.sort(binaries);
LauncherBinary working = null;
Class<?> clazz = null;
for (LauncherBinary binary : binaries) {
File testFile = binary.getPath();
try {
testFile = binary.getExecutableJar();
Bootstrap.log.info("Trying " + testFile.getAbsolutePath() + "...");
clazz = load(testFile);
Bootstrap.log.info("Launcher loaded successfully.");
working = binary;
break;
} catch (Throwable t) {
Bootstrap.log.log(Level.WARNING, "Failed to load " + testFile.getAbsoluteFile(), t);
}
}
if (working != null) {
for (LauncherBinary binary : binaries) {
if (working != binary) {
log.info("Removing " + binary.getPath() + "...");
binary.remove();
}
}
execute(clazz);
} else {
if (redownload) {
launchInitial();
} else {
throw new IOException("Failed to find launchable .jar");
}
}
}
public void execute(Class<?> clazz) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException {
Method method = clazz.getDeclaredMethod("main", String[].class);
String[] launcherArgs;
if (portable) {
launcherArgs = new String[] {
"--portable",
"--dir",
baseDir.getAbsolutePath(),
"--bootstrap-version",
String.valueOf(BOOTSTRAP_VERSION) };
} else {
launcherArgs = new String[] {
"--dir",
baseDir.getAbsolutePath(),
"--bootstrap-version",
String.valueOf(BOOTSTRAP_VERSION) };
}
String[] args = new String[originalArgs.length + launcherArgs.length];
System.arraycopy(launcherArgs, 0, args, 0, launcherArgs.length);
System.arraycopy(originalArgs, 0, args, launcherArgs.length, originalArgs.length);
log.info("Launching with arguments " + Arrays.toString(args));
method.invoke(null, new Object[] { args });
}
public Class<?> load(File jarFile) throws MalformedURLException, ClassNotFoundException {
URL[] urls = new URL[] { jarFile.toURI().toURL() };
URLClassLoader child = new URLClassLoader(urls, this.getClass().getClassLoader());
Class<?> clazz = Class.forName(getProperties().getProperty("launcherClass"), true, child);
return clazz;
}
public static void setSwingLookAndFeel() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Throwable e) {
}
}
private static File getFileChooseDefaultDir() {
JFileChooser chooser = new JFileChooser();
FileSystemView fsv = chooser.getFileSystemView();
return fsv.getDefaultDirectory();
}
private File getUserLauncherDir() {
String osName = System.getProperty("os.name").toLowerCase();
if (osName.contains("win")) {
return new File(getFileChooseDefaultDir(), getProperties().getProperty("homeFolderWindows"));
}
File dotFolder = new File(System.getProperty("user.home"), getProperties().getProperty("homeFolder"));
String xdgFolderName = getProperties().getProperty("homeFolderLinux");
if (osName.contains("linux") && !dotFolder.exists() && xdgFolderName != null && !xdgFolderName.isEmpty()) {
String xdgDataHome = System.getenv("XDG_DATA_HOME");
if (xdgDataHome.isEmpty()) {
xdgDataHome = System.getProperty("user.home") + "/.local/share";
}
return new File(xdgDataHome, xdgFolderName);
}
return dotFolder;
}
private static boolean isPortableMode() {
return new File("portable.txt").exists();
}
}

View file

@ -0,0 +1,49 @@
/*
* SK's Minecraft Launcher
* Copyright (C) 2010-2014 Albert Pham <http://www.sk89q.com> and contributors
* Please see LICENSE.txt for license information.
*/
package com.skcraft.launcher.bootstrap;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
import java.util.regex.Pattern;
public final class BootstrapUtils {
private static final Pattern absoluteUrlPattern = Pattern.compile("^[A-Za-z0-9\\-]+://.*$");
private BootstrapUtils() {
}
public static void checkInterrupted() throws InterruptedException {
if (Thread.interrupted()) {
throw new InterruptedException();
}
}
public static void closeQuietly(Closeable closeable) {
try {
if (closeable != null) {
closeable.close();
}
} catch (IOException e) {
}
}
public static Properties loadProperties(Class<?> clazz, String name) throws IOException {
Properties prop = new Properties();
InputStream in = null;
try {
in = clazz.getResourceAsStream(name);
prop.load(in);
} finally {
closeQuietly(in);
}
return prop;
}
}

View file

@ -0,0 +1,149 @@
/*
* SK's Minecraft Launcher
* Copyright (C) 2010-2014 Albert Pham <http://www.sk89q.com> and contributors
* Please see LICENSE.txt for license information.
*/
package com.skcraft.launcher.bootstrap;
import com.skcraft.launcher.Bootstrap;
import lombok.extern.java.Log;
import javax.swing.*;
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.Timer;
import java.util.TimerTask;
import static com.skcraft.launcher.bootstrap.SharedLocale.tr;
@Log
public class DownloadFrame extends JFrame {
private Downloader downloader;
private Timer timer;
private final JLabel label = new JLabel();
private final JPanel progressPanel = new JPanel(new BorderLayout(0, 5));
private final JPanel textAreaPanel = new JPanel(new BorderLayout());
private final JProgressBar progressBar = new JProgressBar();
private final LinedBoxPanel buttonsPanel = new LinedBoxPanel(true);
private final JButton cancelButton = new JButton(tr("button.cancal"));
public DownloadFrame(ProgressObservable observable) {
super(tr("downloader.title"));
setResizable(false);
initComponents();
label.setText(tr("downloader.pleaseWait"));
setMinimumSize(new Dimension(400, 100));
pack();
setLocationRelativeTo(null);
SwingHelper.setIconImage(this, Bootstrap.class, "bootstrapper_icon.png");
setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent event) {
if (confirmCancel()) {
cancel();
dispose();
}
}
});
}
private void initComponents() {
buttonsPanel.addGlue();
buttonsPanel.addElement(cancelButton);
buttonsPanel.setBorder(BorderFactory.createEmptyBorder(30, 13, 13, 13));;
progressBar.setIndeterminate(true);
progressBar.setMinimum(0);
progressBar.setMaximum(1000);
progressBar.setPreferredSize(new Dimension(0, 16));
progressPanel.add(label, BorderLayout.NORTH);
progressPanel.setBorder(BorderFactory.createEmptyBorder(13, 13, 0, 13));
progressPanel.add(progressBar, BorderLayout.CENTER);
textAreaPanel.setBorder(BorderFactory.createEmptyBorder(10, 13, 0, 13));
add(progressPanel, BorderLayout.NORTH);
add(textAreaPanel, BorderLayout.CENTER);
add(buttonsPanel, BorderLayout.SOUTH);
textAreaPanel.setVisible(false);
cancelButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
if (confirmCancel()) {
cancel();
dispose();
}
}
});
}
private boolean confirmCancel() {
return SwingHelper.confirmDialog(this, tr("progress.confirmCancel"), tr("progress.confirmCancelTitle"));
}
private void cancel() {
Downloader downloader = this.downloader;
if (downloader != null) {
downloader.cancel();
} else {
System.exit(0);
}
}
public synchronized void setDownloader(Downloader downloader) {
this.downloader = downloader;
if (downloader == null) {
if (timer != null) {
timer.cancel();
timer = null;
}
} else {
if (timer == null) {
timer = new Timer();
timer.scheduleAtFixedRate(new UpdateProgress( downloader), 500, 500);
}
}
}
private class UpdateProgress extends TimerTask {
private final Downloader downloader;
public UpdateProgress(Downloader downloader) {
this.downloader = downloader;
}
@Override
public void run() {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
Downloader downloader = DownloadFrame.this.downloader;
if (downloader != null) {
label.setText(downloader.getStatus());
double progress = downloader.getProgress();
if (progress < 0) {
progressBar.setIndeterminate(true);
} else {
progressBar.setIndeterminate(false);
progressBar.setValue((int) (1000 * progress));
}
} else {
label.setText(tr("downloader.pleaseWait"));
progressBar.setIndeterminate(true);
}
}
});
}
}
}

View file

@ -0,0 +1,145 @@
/*
* SK's Minecraft Launcher
* Copyright (C) 2010-2014 Albert Pham <http://www.sk89q.com> and contributors
* Please see LICENSE.txt for license information.
*/
package com.skcraft.launcher.bootstrap;
import com.skcraft.launcher.Bootstrap;
import lombok.extern.java.Log;
import org.json.simple.JSONObject;
import org.json.simple.JSONValue;
import javax.swing.*;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import static com.skcraft.launcher.bootstrap.BootstrapUtils.checkInterrupted;
import static com.skcraft.launcher.bootstrap.SharedLocale.tr;
@Log
public class Downloader implements Runnable, ProgressObservable {
private final Bootstrap bootstrap;
private DownloadFrame dialog;
private HttpRequest httpRequest;
private Thread thread;
public Downloader(Bootstrap bootstrap) {
this.bootstrap = bootstrap;
}
@Override
public void run() {
this.thread = Thread.currentThread();
try {
execute();
} catch (InterruptedException e) {
log.log(Level.WARNING, "Interrupted");
System.exit(0);
} catch (Throwable t) {
log.log(Level.WARNING, "Failed to download launcher", t);
SwingHelper.showErrorDialog(null, tr("errors.failedDownloadError"), tr("errorTitle"), t);
System.exit(0);
}
}
private void execute() throws Exception {
SwingUtilities.invokeAndWait(new Runnable() {
@Override
public void run() {
Bootstrap.setSwingLookAndFeel();
dialog = new DownloadFrame(Downloader.this);
dialog.setVisible(true);
dialog.setDownloader(Downloader.this);
}
});
URL updateUrl = HttpRequest.url(bootstrap.getProperties().getProperty("latestUrl"));
log.info("Reading update URL " + updateUrl + "...");
List<LauncherBinary> binaries = new ArrayList<LauncherBinary>();
try {
String data = HttpRequest
.get(updateUrl)
.execute()
.expectResponseCode(200)
.returnContent()
.asString("UTF-8");
Object object = JSONValue.parse(data);
URL url;
if (object instanceof JSONObject) {
String rawUrl = String.valueOf(((JSONObject) object).get("url"));
if (rawUrl != null) {
url = HttpRequest.url(rawUrl.trim());
} else {
log.warning("Did not get valid update document - got:\n\n" + data);
throw new IOException("Update URL did not return a valid result");
}
} else {
log.warning("Did not get valid update document - got:\n\n" + data);
throw new IOException("Update URL did not return a valid result");
}
checkInterrupted();
File finalFile = new File(bootstrap.getBinariesDir(), System.currentTimeMillis() + ".jar");
File tempFile = new File(finalFile.getParentFile(), finalFile.getName() + ".tmp");
log.info("Downloading " + url + " to " + tempFile.getAbsolutePath());
httpRequest = HttpRequest.get(url);
httpRequest
.execute()
.expectResponseCode(200)
.saveContent(tempFile);
finalFile.delete();
tempFile.renameTo(finalFile);
LauncherBinary binary = new LauncherBinary(finalFile);
binaries.add(binary);
} finally {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
dialog.setDownloader(null);
dialog.dispose();
}
});
}
bootstrap.launchExisting(binaries, false);
}
public void cancel() {
thread.interrupt();
}
public String getStatus() {
HttpRequest httpRequest = this.httpRequest;
if (httpRequest != null) {
double progress = httpRequest.getProgress();
if (progress >= 0) {
return String.format(tr("downloader.progressStatus"), progress * 100);
}
}
return tr("downloader.status");
}
@Override
public double getProgress() {
HttpRequest httpRequest = this.httpRequest;
return httpRequest != null ? httpRequest.getProgress() : -1;
}
}

View file

@ -0,0 +1,504 @@
/*
* SK's Minecraft Launcher
* Copyright (C) 2010-2014 Albert Pham <http://www.sk89q.com> and contributors
* Please see LICENSE.txt for license information.
*/
package com.skcraft.launcher.bootstrap;
import lombok.Getter;
import lombok.extern.java.Log;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static com.skcraft.launcher.bootstrap.BootstrapUtils.checkInterrupted;
import static com.skcraft.launcher.bootstrap.BootstrapUtils.closeQuietly;
/**
* A simple fluent interface for performing HTTP requests that uses
* {@link java.net.HttpURLConnection} or {@link javax.net.ssl.HttpsURLConnection}.
*/
@Log
public class HttpRequest implements Closeable, ProgressObservable {
private static final int READ_TIMEOUT = 1000 * 60 * 10;
private static final int READ_BUFFER_SIZE = 1024 * 8;
private final Map<String, String> headers = new HashMap<String, String>();
private String method;
@Getter
private final URL url;
private String contentType;
private byte[] body;
private HttpURLConnection conn;
private InputStream inputStream;
private int redirectCount;
private long contentLength = -1;
private long readBytes = 0;
/**
* Create a new HTTP request.
*
* @param method the method
* @param url the URL
*/
private HttpRequest(String method, URL url) {
this.method = method;
this.url = url;
}
/**
* Submit form data.
*
* @param form the form
* @return this object
*/
public HttpRequest bodyForm(Form form) {
contentType = "application/x-www-form-urlencoded";
body = form.toString().getBytes();
return this;
}
/**
* Add a header.
*
* @param key the header key
* @param value the header value
* @return this object
*/
public HttpRequest header(String key, String value) {
headers.put(key, value);
return this;
}
/**
* Execute the request.
* <p/>
* After execution, {@link #close()} should be called.
*
* @return this object
* @throws java.io.IOException on I/O error
*/
public HttpRequest execute() throws IOException {
boolean successful = false;
try {
if (conn != null) {
throw new IllegalArgumentException("Connection already executed");
}
conn = this.runRequest(url);
inputStream = conn.getResponseCode() == HttpURLConnection.HTTP_OK ?
conn.getInputStream() : conn.getErrorStream();
successful = true;
} finally {
if (!successful) {
close();
}
}
return this;
}
private HttpURLConnection runRequest(URL url) throws IOException {
if (redirectCount > 20) {
throw new IOException("Too many redirects!");
}
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestProperty("User-Agent", "Mozilla/5.0 (Java) SKMCLauncher");
conn.setInstanceFollowRedirects(false);
if (body != null) {
conn.setRequestProperty("Content-Type", contentType);
conn.setRequestProperty("Content-Length", Integer.toString(body.length));
conn.setDoInput(true);
}
for (Map.Entry<String, String> entry : headers.entrySet()) {
conn.setRequestProperty(entry.getKey(), entry.getValue());
}
conn.setRequestMethod(method);
conn.setUseCaches(false);
conn.setDoOutput(true);
conn.setReadTimeout(READ_TIMEOUT);
conn.connect();
if (body != null) {
DataOutputStream out = new DataOutputStream(conn.getOutputStream());
out.write(body);
out.flush();
out.close();
}
switch (conn.getResponseCode()) {
case HttpURLConnection.HTTP_SEE_OTHER:
method = "GET";
body = null;
case HttpURLConnection.HTTP_MOVED_PERM:
case HttpURLConnection.HTTP_MOVED_TEMP:
case HttpURLConnection.HTTP_ACCEPTED:
case 307:
case 308:
String location = conn.getHeaderField("Location");
redirectCount++;
return runRequest(new URL(this.url, location));
default:
break;
}
return conn;
}
/**
* Require that the response code is one of the given response codes.
*
* @param codes a list of codes
* @return this object
* @throws java.io.IOException if there is an I/O error or the response code is not expected
*/
public HttpRequest expectResponseCode(int... codes) throws IOException {
int responseCode = getResponseCode();
for (int code : codes) {
if (code == responseCode) {
return this;
}
}
close();
throw new IOException("Did not get expected response code, got " + responseCode + " for " + url);
}
/**
* Get the response code.
*
* @return the response code
* @throws java.io.IOException on I/O error
*/
public int getResponseCode() throws IOException {
if (conn == null) {
throw new IllegalArgumentException("No connection has been made");
}
return conn.getResponseCode();
}
/**
* Get the input stream.
*
* @return the input stream
*/
public InputStream getInputStream() {
return inputStream;
}
/**
* Buffer the returned response.
*
* @return the buffered response
* @throws java.io.IOException on I/O error
* @throws InterruptedException on interruption
*/
public BufferedResponse returnContent() throws IOException, InterruptedException {
if (inputStream == null) {
throw new IllegalArgumentException("No input stream available");
}
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
int b = 0;
while ((b = inputStream.read()) != -1) {
checkInterrupted();
bos.write(b);
}
return new BufferedResponse(bos.toByteArray());
} finally {
close();
}
}
/**
* Save the result to a file.
*
* @param file the file
* @return this object
* @throws java.io.IOException on I/O error
* @throws InterruptedException on interruption
*/
public HttpRequest saveContent(File file) throws IOException, InterruptedException {
FileOutputStream fos = null;
BufferedOutputStream bos = null;
try {
fos = new FileOutputStream(file);
bos = new BufferedOutputStream(fos);
saveContent(bos);
} finally {
closeQuietly(bos);
closeQuietly(fos);
}
return this;
}
/**
* Save the result to an output stream.
*
* @param out the output stream
* @return this object
* @throws java.io.IOException on I/O error
* @throws InterruptedException on interruption
*/
public HttpRequest saveContent(OutputStream out) throws IOException, InterruptedException {
BufferedInputStream bis;
try {
String field = conn.getHeaderField("Content-Length");
if (field != null) {
long len = Long.parseLong(field);
if (len >= 0) { // Let's just not deal with really big numbers
contentLength = len;
}
}
} catch (NumberFormatException e) {
}
try {
bis = new BufferedInputStream(inputStream);
byte[] data = new byte[READ_BUFFER_SIZE];
int len = 0;
while ((len = bis.read(data, 0, READ_BUFFER_SIZE)) >= 0) {
out.write(data, 0, len);
readBytes += len;
checkInterrupted();
}
} finally {
close();
}
return this;
}
@Override
public double getProgress() {
if (contentLength >= 0) {
return readBytes / (double) contentLength;
} else {
return -1;
}
}
@Override
public void close() throws IOException {
if (conn != null) conn.disconnect();
}
/**
* Perform a GET request.
*
* @param url the URL
* @return a new request object
*/
public static HttpRequest get(URL url) {
return request("GET", url);
}
/**
* Perform a POST request.
*
* @param url the URL
* @return a new request object
*/
public static HttpRequest post(URL url) {
return request("POST", url);
}
/**
* Perform a request.
*
* @param method the method
* @param url the URL
* @return a new request object
*/
public static HttpRequest request(String method, URL url) {
return new HttpRequest(method, url);
}
/**
* Create a new {@link java.net.URL} and throw a {@link RuntimeException} if the URL
* is not valid.
*
* @param url the url
* @return a URL object
* @throws RuntimeException if the URL is invalid
*/
public static URL url(String url) {
try {
return new URL(url);
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
}
/**
* Used with {@link #bodyForm(Form)}.
*/
public final static class Form {
public final List<String> elements = new ArrayList<String>();
private Form() {
}
/**
* Add a key/value to the form.
*
* @param key the key
* @param value the value
* @return this object
*/
public Form add(String key, String value) {
try {
elements.add(URLEncoder.encode(key, "UTF-8") +
"=" + URLEncoder.encode(value, "UTF-8"));
return this;
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
boolean first = true;
for (String element : elements) {
if (first) {
first = false;
} else {
builder.append("&");
}
builder.append(element);
}
return builder.toString();
}
/**
* Create a new form.
*
* @return a new form
*/
public static Form form() {
return new Form();
}
}
/**
* Used to buffer the response in memory.
*/
public class BufferedResponse {
private final byte[] data;
private BufferedResponse(byte[] data) {
this.data = data;
}
/**
* Return the result as bytes.
*
* @return the data
*/
public byte[] asBytes() {
return data;
}
/**
* Return the result as a string.
*
* @param encoding the encoding
* @return the string
* @throws java.io.IOException on I/O error
*/
public String asString(String encoding) throws IOException {
return new String(data, encoding);
}
/**
* Return the result as an instance of the given class that has been
* deserialized from a XML payload.
*
* @return the object
* @throws java.io.IOException on I/O error
*/
@SuppressWarnings("unchecked")
public <T> T asXml(Class<T> cls) throws IOException {
try {
JAXBContext context = JAXBContext.newInstance(cls);
Unmarshaller um = context.createUnmarshaller();
return (T) um.unmarshal(new ByteArrayInputStream(data));
} catch (JAXBException e) {
throw new IOException(e);
}
}
/**
* Save the result to a file.
*
* @param file the file
* @return this object
* @throws java.io.IOException on I/O error
* @throws InterruptedException on interruption
*/
public BufferedResponse saveContent(File file) throws IOException, InterruptedException {
FileOutputStream fos = null;
BufferedOutputStream bos = null;
file.getParentFile().mkdirs();
try {
fos = new FileOutputStream(file);
bos = new BufferedOutputStream(fos);
saveContent(bos);
} finally {
closeQuietly(bos);
closeQuietly(fos);
}
return this;
}
/**
* Save the result to an output stream.
*
* @param out the output stream
* @return this object
* @throws java.io.IOException on I/O error
* @throws InterruptedException on interruption
*/
public BufferedResponse saveContent(OutputStream out) throws IOException, InterruptedException {
out.write(data);
return this;
}
}
}

View file

@ -0,0 +1,81 @@
/*
* SK's Minecraft Launcher
* Copyright (C) 2010-2014 Albert Pham <http://www.sk89q.com> and contributors
* Please see LICENSE.txt for license information.
*/
package com.skcraft.launcher.bootstrap;
import lombok.Getter;
import lombok.extern.java.Log;
import java.io.File;
import java.io.FileFilter;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Log
public class LauncherBinary implements Comparable<LauncherBinary> {
public static final Pattern PATTERN = Pattern.compile("^([0-9]+)\\.jar(\\.pack)?$");
@Getter
private final File path;
private final long time;
private final boolean packed;
public LauncherBinary(File path) {
this.path = path;
String name = path.getName();
Matcher m = PATTERN.matcher(name);
if (!m.matches()) {
throw new IllegalArgumentException("Invalid filename: " + path);
}
time = Long.parseLong(m.group(1));
packed = m.group(2) != null;
}
public File getExecutableJar() throws PackedJarException {
if (packed) {
log.warning("Launcher binary " + path.getAbsolutePath() + " is a pack200 file, which is " +
"no longer supported.");
throw new PackedJarException("Cannot unpack .jar.pack files!");
} else {
return path;
}
}
@Override
public int compareTo(LauncherBinary o) {
if (time > o.time) {
return -1;
} else if (time < o.time) {
return 1;
} else {
if (packed && !o.packed) {
return 1;
} else if (!packed && o.packed) {
return -1;
} else {
return 0;
}
}
}
public void remove() {
path.delete();
}
public static class Filter implements FileFilter {
@Override
public boolean accept(File file) {
return file.isFile() && LauncherBinary.PATTERN.matcher(file.getName()).matches();
}
}
public static class PackedJarException extends Exception {
public PackedJarException(String message) {
super(message);
}
}
}

View file

@ -0,0 +1,52 @@
/*
* SK's Minecraft Launcher
* Copyright (C) 2010-2014 Albert Pham <http://www.sk89q.com> and contributors
* Please see LICENSE.txt for license information.
*/
package com.skcraft.launcher.bootstrap;
import lombok.Getter;
import lombok.Setter;
import javax.swing.*;
import java.awt.*;
public class LinedBoxPanel extends JPanel {
@Getter
private final boolean horizontal;
@Getter @Setter
private int spacing = 6;
private boolean needsSpacer = false;
public LinedBoxPanel(boolean horizontal) {
this.horizontal = horizontal;
setLayout(new BoxLayout(this,
horizontal ? BoxLayout.X_AXIS : BoxLayout.Y_AXIS));
setBorder(BorderFactory.createEmptyBorder(0, 10, 10, 10));
}
public LinedBoxPanel fullyPadded() {
setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
return this;
}
public void addElement(Component component) {
if (needsSpacer) {
add(horizontal ?
Box.createHorizontalStrut(spacing) :
Box.createVerticalStrut(spacing));
}
add(component);
needsSpacer = true;
}
public void addGlue() {
add(horizontal ?
Box.createHorizontalGlue() :
Box.createVerticalGlue());
needsSpacer = false;
}
}

View file

@ -0,0 +1,23 @@
/*
* SK's Minecraft Launcher
* Copyright (C) 2010-2014 Albert Pham <http://www.sk89q.com> and contributors
* Please see LICENSE.txt for license information.
*/
package com.skcraft.launcher.bootstrap;
/**
* Implementations of this interface can provide information on the progress
* of a task.
*/
public interface ProgressObservable {
/**
* Get the progress value as a number between 0 and 1 (inclusive), or -1
* if progress information is unavailable.
*
* @return the progress value
*/
double getProgress();
}

View file

@ -0,0 +1,106 @@
/*
* SK's Minecraft Launcher
* Copyright (C) 2010-2014 Albert Pham <http://www.sk89q.com> and contributors
* Please see LICENSE.txt for license information.
*/
package com.skcraft.launcher.bootstrap;
import lombok.NonNull;
import lombok.extern.java.Log;
import java.text.MessageFormat;
import java.util.Locale;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.logging.Level;
/**
* Handles loading a shared message {@link ResourceBundle}.
*/
@Log
public class SharedLocale {
private static Locale locale = Locale.getDefault();
private static ResourceBundle bundle;
/**
* Get the current locale.
*
* @return the current locale
*/
public static Locale getLocale() {
return locale;
}
/**
* Get the current resource bundle.
*
* @return the current resource bundle, or null if not available
*/
public static ResourceBundle getBundle() {
return bundle;
}
/**
* Translate a string.
*
* <p>If the string is not available, then ${key} will be returned.</p>
*
* @param key the key
* @return the translated string
*/
public static String tr(String key) {
if (bundle != null) {
try {
return bundle.getString(key);
} catch (MissingResourceException e) {
log.log(Level.WARNING, "Failed to find message", e);
}
}
return "${" + key + "}";
}
/**
* Format a translated string.
*
* <p>If the string is not available, then ${key}:args will be returned.</p>
*
* @param key the key
* @param args arguments
* @return a translated string
*/
public static String tr(String key, Object... args) {
if (bundle != null) {
try {
MessageFormat formatter = new MessageFormat(tr(key));
formatter.setLocale(getLocale());
return formatter.format(args);
} catch (MissingResourceException e) {
log.log(Level.WARNING, "Failed to find message", e);
}
}
return "${" + key + "}:" + args;
}
/**
* Load a shared resource bundle.
*
* @param baseName the bundle name
* @param locale the locale
* @return true if loaded successfully
*/
public static boolean loadBundle(@NonNull String baseName, @NonNull Locale locale) {
try {
SharedLocale.locale = locale;
bundle = ResourceBundle.getBundle(baseName, locale,
SharedLocale.class.getClassLoader());
return true;
} catch (MissingResourceException e) {
log.log(Level.SEVERE, "Failed to load resource bundle", e);
return false;
}
}
}

View file

@ -0,0 +1,63 @@
/*
* SK's Minecraft Launcher
* Copyright (C) 2010-2014 Albert Pham <http://www.sk89q.com> and contributors
* Please see LICENSE.txt for license information.
*/
package com.skcraft.launcher.bootstrap;
import lombok.extern.java.Log;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.logging.*;
@Log
public final class SimpleLogFormatter extends Formatter {
private static final String LINE_SEPARATOR = System.getProperty("line.separator");
@Override
public String format(LogRecord record) {
StringBuilder sb = new StringBuilder();
sb.append("[")
.append(record.getLevel().getLocalizedName().toLowerCase())
.append("] ")
.append(formatMessage(record))
.append(LINE_SEPARATOR);
if (record.getThrown() != null) {
try {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
record.getThrown().printStackTrace(pw);
pw.close();
sb.append(sw.toString());
} catch (Exception e) {
}
}
return sb.toString();
}
public static void configureGlobalLogger() {
Logger globalLogger = Logger.getLogger("");
// Set formatter
for (Handler handler : globalLogger.getHandlers()) {
handler.setFormatter(new SimpleLogFormatter());
}
// Set level
String logLevel = System.getProperty(
SimpleLogFormatter.class.getCanonicalName() + ".logLevel", "INFO");
try {
Level level = Level.parse(logLevel);
globalLogger.setLevel(level);
} catch (IllegalArgumentException e) {
log.log(Level.WARNING, "Invalid log level of " + logLevel, e);
}
}
}

View file

@ -0,0 +1,227 @@
/*
* SK's Minecraft Launcher
* Copyright (C) 2010-2014 Albert Pham <http://www.sk89q.com> and contributors
* Please see LICENSE.txt for license information.
*/
package com.skcraft.launcher.bootstrap;
import lombok.NonNull;
import lombok.extern.java.Log;
import javax.imageio.ImageIO;
import javax.swing.*;
import javax.swing.text.JTextComponent;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.InvocationTargetException;
import java.util.concurrent.atomic.AtomicBoolean;
import static com.skcraft.launcher.bootstrap.BootstrapUtils.closeQuietly;
import static com.skcraft.launcher.bootstrap.SharedLocale.tr;
/**
* Swing utility methods.
*/
@Log
public final class SwingHelper {
private SwingHelper() {
}
public static String htmlEscape(String str) {
return str.replace(">", "&gt;")
.replace("<", "&lt;")
.replace("&", "&amp;");
}
/**
* Shows an popup error dialog, with potential extra details shown either immediately
* or available on the dialog.
*
* @param parentComponent the frame from which the dialog is displayed, otherwise
* null to use the default frame
* @param message the message to display
* @param title the title string for the dialog
* @see #showMessageDialog(java.awt.Component, String, String, String, int) for details
*/
public static void showErrorDialog(Component parentComponent, @NonNull String message,
@NonNull String title) {
showErrorDialog(parentComponent, message, title, null);
}
/**
* Shows an popup error dialog, with potential extra details shown either immediately
* or available on the dialog.
*
* @param parentComponent the frame from which the dialog is displayed, otherwise
* null to use the default frame
* @param message the message to display
* @param title the title string for the dialog
* @param throwable the exception, or null if there is no exception to show
* @see #showMessageDialog(java.awt.Component, String, String, String, int) for details
*/
public static void showErrorDialog(Component parentComponent, @NonNull String message,
@NonNull String title, Throwable throwable) {
String detailsText = null;
// Get a string version of the exception and use that for
// the extra details text
if (throwable != null) {
StringWriter sw = new StringWriter();
throwable.printStackTrace(new PrintWriter(sw));
detailsText = sw.toString();
}
showMessageDialog(parentComponent,
message, title,
detailsText, JOptionPane.ERROR_MESSAGE);
}
/**
* Show a message dialog using
* {@link javax.swing.JOptionPane#showMessageDialog(java.awt.Component, Object, String, int)}.
*
* <p>The dialog will be shown from the Event Dispatch Thread, regardless of the
* thread it is called from. In either case, the method will block until the
* user has closed the dialog (or dialog creation fails for whatever reason).</p>
*
* @param parentComponent the frame from which the dialog is displayed, otherwise
* null to use the default frame
* @param message the message to display
* @param title the title string for the dialog
* @param messageType see {@link javax.swing.JOptionPane#showMessageDialog(java.awt.Component, Object, String, int)}
* for available message types
*/
public static void showMessageDialog(final Component parentComponent,
@NonNull final String message,
@NonNull final String title,
final String detailsText,
final int messageType) {
if (SwingUtilities.isEventDispatchThread()) {
// To force the label to wrap, convert the message to broken HTML
String htmlMessage = "<html><div style=\"width: 250px\">" + htmlEscape(message);
JPanel panel = new JPanel(new BorderLayout(0, detailsText != null ? 20 : 0));
// Add the main message
panel.add(new JLabel(htmlMessage), BorderLayout.NORTH);
// Add the extra details
if (detailsText != null) {
JTextArea textArea = new JTextArea(tr("errorDialog.reportError") + "\n\n" + detailsText);
JLabel tempLabel = new JLabel();
textArea.setFont(tempLabel.getFont());
textArea.setBackground(tempLabel.getBackground());
textArea.setTabSize(2);
textArea.setEditable(false);;
JScrollPane scrollPane = new JScrollPane(textArea);
scrollPane.setPreferredSize(new Dimension(350, 120));
panel.add(scrollPane, BorderLayout.CENTER);
}
JOptionPane.showMessageDialog(
parentComponent, panel, title, messageType);
} else {
// Call method again from the Event Dispatch Thread
try {
SwingUtilities.invokeAndWait(new Runnable() {
@Override
public void run() {
showMessageDialog(
parentComponent, message, title,
detailsText, messageType);
}
});
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
}
}
}
/**
* Asks the user a binary yes or no question.
*
* @param parentComponent the component
* @param message the message to display
* @param title the title string for the dialog
* @return whether 'yes' was selected
*/
public static boolean confirmDialog(final Component parentComponent,
@NonNull final String message,
@NonNull final String title) {
if (SwingUtilities.isEventDispatchThread()) {
return JOptionPane.showConfirmDialog(
parentComponent, message, title, JOptionPane.YES_NO_OPTION) ==
JOptionPane.YES_OPTION;
} else {
// Use an AtomicBoolean to pass the result back from the
// Event Dispatcher Thread
final AtomicBoolean yesSelected = new AtomicBoolean();
try {
SwingUtilities.invokeAndWait(new Runnable() {
@Override
public void run() {
yesSelected.set(confirmDialog(parentComponent, title, message));
}
});
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
}
return yesSelected.get();
}
}
public static BufferedImage readIconImage(Class<?> clazz, String path) {
InputStream in = null;
try {
in = clazz.getResourceAsStream(path);
if (in != null) {
return ImageIO.read(in);
}
} catch (IOException e) {
} finally {
closeQuietly(in);
}
return null;
}
public static void setIconImage(Window frame, Class<?> clazz, String path) {
BufferedImage image = readIconImage(clazz, path);
if (image != null) {
frame.setIconImage(image);
}
}
/**
* Focus a component.
*
* <p>The focus call happens in {@link javax.swing.SwingUtilities#invokeLater(Runnable)}.</p>
*
* @param component the component
*/
public static void focusLater(@NonNull final Component component) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
if (component instanceof JTextComponent) {
((JTextComponent) component).selectAll();
}
component.requestFocusInWindow();
}
});
}
}

View file

@ -0,0 +1,11 @@
#
# SK's Minecraft Launcher
# Copyright (C) 2010-2014 Albert Pham <http://www.sk89q.com> and contributors
# Please see LICENSE.txt for license information.
#
homeFolderWindows=Example Launcher
homeFolderLinux=example_launcher
homeFolder=.examplelauncher
launcherClass=com.skcraft.launcher.Launcher
latestUrl=http://update.skcraft.com/quark/launcher/latest.json

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

View file

@ -0,0 +1,21 @@
#
# SK's Minecraft Launcher
# Copyright (C) 2010-2014 Albert Pham <http://www.sk89q.com> and contributors
# Please see LICENSE.txt for license information.
#
button.cancal=Cancel
errorTitle=Error
errors.failedDownloadError=The SKCraft launcher could not be downloaded.
errors.bootstrapError=An error occurred while trying to bootstrap the launcher\!
errorDialog.reportError=To report this error, provide\:
downloader.title=Downloading SKCraft Launcher...
downloader.pleaseWait=Please wait...
downloader.status=Downloading latest SKCraft Launcher...
downloader.progressStatus=Downloading latest SKCraft Launcher (%.2f%%)...
progress.confirmCancel=Are you sure that you wish to cancel?
progress.confirmCancelTitle=Cancel