/* * This file is part of the Illarion project. * * Copyright © 2015 - Illarion e.V. * * Illarion is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Illarion is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. */ package illarion.download.gui.controller; import illarion.common.config.Config; import illarion.common.util.ProgressMonitor; import illarion.common.util.ProgressMonitorCallback; import illarion.download.cleanup.Cleaner; import illarion.download.launcher.JavaLauncher; import illarion.download.maven.MavenDownloader; import illarion.download.maven.MavenDownloaderCallback; import javafx.application.Platform; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.fxml.FXML; import javafx.geometry.Pos; import javafx.scene.Cursor; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.OverrunStyle; import javafx.scene.control.ProgressBar; import javafx.scene.input.*; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.BorderPane; import javafx.scene.layout.Pane; import javafx.scene.layout.VBox; import org.jetbrains.annotations.Contract; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserFactory; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.io.BufferedInputStream; import java.io.File; import java.io.IOException; import java.net.SocketTimeoutException; import java.net.URL; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.*; /** * @author Martin Karing <nitram@illarion.org> */ public class MainViewController extends AbstractController implements MavenDownloaderCallback, ProgressMonitorCallback { @FXML public AnchorPane newsPane; @FXML public AnchorPane questsPane; @FXML public ProgressBar progress; @FXML public Label progressDescription; @FXML public Button launchEasyNpcButton; @FXML public Button launchEasyQuestButton; @FXML public Button launchMapEditButton; @FXML public Button launchClientButton; private ResourceBundle resourceBundle; @Nonnull private static final Logger log = LoggerFactory.getLogger(MainViewController.class); @Override public void initialize(URL location, @Nonnull ResourceBundle resources) { resourceBundle = resources; progress.setProgress(0.0); progressDescription.setText(resources.getString("selectStartApp")); new Thread(() -> { try { readNewsAndQuests(); } catch (@Nonnull XmlPullParserException | IOException | ParseException e) { log.error("Failed reading news and quests.", e); } }).start(); KeyCombination combo = new KeyCodeCombination(KeyCode.ENTER); EventHandler<KeyEvent> eventEventHandler = event -> { if (combo.match(event)) { launchClientButton.fire(); } event.consume(); }; launchEasyNpcButton.setOnKeyReleased(eventEventHandler); launchEasyQuestButton.setOnKeyReleased(eventEventHandler); launchMapEditButton.setOnKeyReleased(eventEventHandler); launchClientButton.requestFocus(); } @Nullable private String launchClass; private boolean useSnapshots; private static class NewsQuestEntry implements Comparable<NewsQuestEntry> { @Nonnull public final String title; @Nullable public final Date timeStamp; @Nonnull public final URL linkTarget; NewsQuestEntry(@Nonnull String title, @Nullable Date timeStamp, @Nonnull URL linkTarget) { this.title = title; this.timeStamp = (timeStamp == null) ? null : (Date) timeStamp.clone(); this.linkTarget = linkTarget; } @Override public int compareTo(@Nonnull NewsQuestEntry o) { if ((timeStamp == null) && (o.timeStamp != null)) { return -1; } if ((timeStamp != null) && (o.timeStamp == null)) { return 1; } int compare = (timeStamp == null) ? 0 : timeStamp.compareTo(o.timeStamp); if (compare == 0) { return title.compareTo(o.title); } return compare; } } private void readNewsAndQuests() throws XmlPullParserException, IOException, ParseException { XmlPullParserFactory parserFactory = XmlPullParserFactory.newInstance(); parserFactory.setValidating(false); parserFactory.setNamespaceAware(false); XmlPullParser parser = parserFactory.newPullParser(); URL src = new URL("http://illarion.org/data/xml_launcher.php"); parser.setInput(new BufferedInputStream(src.openStream()), "UTF-8"); List<NewsQuestEntry> newsList = new ArrayList<>(); List<NewsQuestEntry> questList = new ArrayList<>(); int current = parser.nextTag(); while ((current != XmlPullParser.START_TAG) || !"launcher".equals(parser.getName())) { current = parser.nextTag(); } parseLauncherXml(parser, newsList, questList); showNewsInList(newsList); showQuestsInList(questList); } private static void parseLauncherXml( @Nonnull XmlPullParser parser, @Nonnull List<NewsQuestEntry> newsList, @Nonnull List<NewsQuestEntry> questList) throws IOException, XmlPullParserException, ParseException { while (true) { int current = parser.nextToken(); switch (current) { case XmlPullParser.END_DOCUMENT: return; case XmlPullParser.END_TAG: if ("launcher".equals(parser.getName())) { return; } break; case XmlPullParser.START_TAG: switch (parser.getName()) { case "news": parserEntryXml(parser, newsList); break; case "quests": parserEntryXml(parser, questList); break; } break; } } } private static void parserEntryXml( @Nonnull XmlPullParser parser, @Nonnull List<NewsQuestEntry> list) throws IOException, XmlPullParserException, ParseException { boolean useGerman = Locale.getDefault().getLanguage().equals(Locale.GERMAN.getLanguage()); String title = null; Date timestamp = null; URL linkTarget = null; DateFormat parsingFormat = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss Z", Locale.ENGLISH); while (true) { int current = parser.nextTag(); switch (current) { case XmlPullParser.END_DOCUMENT: log.error("Reached unexpected end of document."); return; case XmlPullParser.END_TAG: switch (parser.getName()) { case "quests": Collections.sort(list); return; case "news": Collections.sort(list); Collections.reverse(list); return; case "item": list.add(new NewsQuestEntry(title, timestamp, linkTarget)); timestamp = null; title = null; linkTarget = null; break; } break; case XmlPullParser.START_TAG: switch (parser.getName()) { case "id": parser.nextText(); break; case "title": boolean german = "de".equals(parser.getAttributeValue(null, "lang")); String text = parser.nextText(); if (isNullOrEmpty(title) || (!isNullOrEmpty(text) && (german == useGerman))) { title = text; } break; case "link": String linkUrl = parser.nextText(); if (!isNullOrEmpty(linkUrl)) { linkTarget = new URL(linkUrl); } break; case "date": String textTimeStamp = parser.nextText(); if (!isNullOrEmpty(textTimeStamp)) { timestamp = parsingFormat.parse(textTimeStamp); } break; } break; } } } @Contract(value = "null -> true", pure = true) private static boolean isNullOrEmpty(@Nullable String testString) { return (testString == null) || testString.isEmpty(); } private void showNewsInList(@Nonnull Iterable<NewsQuestEntry> list) { showNewsQuestInList(list, newsPane, DateFormat.getDateInstance(DateFormat.MEDIUM)); } private void showQuestsInList(@Nonnull Iterable<NewsQuestEntry> list) { showNewsQuestInList(list, questsPane, DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT)); } private void showNewsQuestInList(@Nonnull Iterable<NewsQuestEntry> list, @Nonnull Pane display, @Nonnull DateFormat dateFormat) { VBox storage = new VBox(); storage.setFillWidth(true); AnchorPane.setBottomAnchor(storage, 0.0); AnchorPane.setTopAnchor(storage, 0.0); AnchorPane.setLeftAnchor(storage, 3.0); AnchorPane.setRightAnchor(storage, 3.0); int entryCount = 0; for (@Nonnull NewsQuestEntry entry : list) { if (entryCount == 4) { break; } entryCount += 1; BorderPane line = new BorderPane(); line.getStyleClass().add("linkPane"); Label title = new Label(entry.title); title.setTextOverrun(OverrunStyle.WORD_ELLIPSIS); line.setCenter(title); BorderPane.setAlignment(title, Pos.BOTTOM_LEFT); if (entry.timeStamp != null) { Label timeStamp = new Label(dateFormat.format(entry.timeStamp)); timeStamp.setTextOverrun(OverrunStyle.CLIP); line.setRight(timeStamp); line.setCursor(Cursor.HAND); } line.setMouseTransparent(false); line.setOnMouseClicked(mouseEvent -> { if ((mouseEvent.getButton() == MouseButton.PRIMARY) && Objects.equals(mouseEvent.getEventType(), MouseEvent.MOUSE_CLICKED) && (mouseEvent.getClickCount() == 1)) { getModel().getHostServices().showDocument(entry.linkTarget.toExternalForm()); } }); storage.getChildren().add(line); } Platform.runLater(() -> display.getChildren().add(storage)); } @FXML public void goToAccount(@Nonnull ActionEvent actionEvent) { getModel().getHostServices().showDocument("http://illarion.org/community/account/index.php"); } @FXML public void startEasyNpc(@Nonnull ActionEvent actionEvent) { updateLaunchButtons(false, false, true, false, false); launch("org.illarion", "easynpc", "illarion.easynpc.gui.MainFrame", "channelEasyNpc"); } @FXML public void startEasyQuest(@Nonnull ActionEvent actionEvent) { updateLaunchButtons(false, false, false, true, false); launch("org.illarion", "easyquest", "illarion.easyquest.gui.MainFrame", "channelEasyQuest"); } @FXML public void startMapEdit(@Nonnull ActionEvent actionEvent) { updateLaunchButtons(false, false, false, false, true); launch("org.illarion", "mapeditor", "illarion.mapedit.MapEditor", "channelMapEditor"); } @FXML public void launchClient(@Nonnull ActionEvent actionEvent) { updateLaunchButtons(false, true, false, false, false); launch("org.illarion", "client", "illarion.client.IllaClient", "channelClient"); } private void updateLaunchButtons( boolean enabled, boolean client, boolean easyNpc, boolean easyQuest, boolean mapEdit) { if (Platform.isFxApplicationThread()) { launchClientButton.setDisable(!enabled); launchMapEditButton.setDisable(!enabled); launchEasyQuestButton.setDisable(!enabled); launchEasyNpcButton.setDisable(!enabled); if (enabled) { launchClientButton.setText(resourceBundle.getString("launchClient")); launchMapEditButton.setText(resourceBundle.getString("launchMapEdit")); launchEasyQuestButton.setText(resourceBundle.getString("launchEasyQuest")); launchEasyNpcButton.setText(resourceBundle.getString("launchEasyNpc")); } else { launchClientButton.setText(resourceBundle.getString(client ? "starting" : "launchClient")); launchMapEditButton.setText(resourceBundle.getString(mapEdit ? "starting" : "launchMapEdit")); launchEasyQuestButton.setText(resourceBundle.getString(easyQuest ? "starting" : "launchEasyQuest")); launchEasyNpcButton.setText(resourceBundle.getString(easyNpc ? "starting" : "launchEasyNpc")); } } else { Platform.runLater(() -> updateLaunchButtons(enabled, client, easyNpc, easyQuest, mapEdit)); } } private void launch( @Nonnull String groupId, @Nonnull String artifactId, @Nonnull String launchClass, @Nonnull String configKey) { Config cfg = getModel().getConfig(); this.launchClass = launchClass; useSnapshots = cfg.getInteger(configKey) == 1; new Thread(() -> { int attempt = 0; while (attempt < 10) { attempt++; try { MavenDownloader downloader = new MavenDownloader(useSnapshots, attempt, cfg); downloader.downloadArtifact(groupId, artifactId, this); } catch (@Nonnull Exception e) { //noinspection ThrowableResultOfMethodCallIgnored if (getInnerExceptionOfType(SocketTimeoutException.class, e) != null) { log.warn("Timeout detected. Restarting download with longer timeout."); continue; } log.error("Error while resolving.", e); } break; } }).start(); } @Nullable private static <T extends Throwable> T getInnerExceptionOfType( @Nonnull Class<T> clazz, @Nonnull Throwable search) { @Nullable Throwable currentEx = search; while (currentEx != null) { if (currentEx.getClass().equals(clazz)) { //noinspection unchecked return (T) currentEx; } currentEx = currentEx.getCause(); } return null; } @Override public void reportNewState( @Nonnull State state, @Nullable ProgressMonitor progress, boolean offline, @Nullable String detail) { if (Platform.isFxApplicationThread()) { switch (state) { case SearchingNewVersion: progressDescription .setText((offline ? "Offline: " : "") + resourceBundle.getString("searchingNewVersion") + ((detail == null) ? "" : (" - " + detail))); break; case ResolvingDependencies: progressDescription .setText((offline ? "Offline: " : "") + resourceBundle.getString("resolvingDependencies") + ((detail == null) ? "" : (" - " + detail))); break; case ResolvingArtifacts: progressDescription .setText((offline ? "Offline: " : "") + resourceBundle.getString("resolvingArtifacts") + ((detail == null) ? "" : (" - " + detail))); break; } if (progress == null) { this.progress.setProgress(-1.0); } else { progress.setCallback(this); this.progress.setProgress(progress.getProgress()); } } else { Platform.runLater(() -> reportNewState(state, progress, offline, detail)); } } @Override public void resolvingDone(@Nonnull Collection<File> classpath) { if (launchClass == null) { cancelLaunch(); Platform.runLater(() -> { progress.setProgress(1.0); progressDescription.setText(resourceBundle.getString("errorClasspathNull")); }); } else { Platform.runLater(() -> { progress.setProgress(1.0); progressDescription.setText(resourceBundle.getString("launchApplication")); }); JavaLauncher launcher = new JavaLauncher(getModel().getConfig(), useSnapshots); if (launcher.launch(classpath, launchClass)) { Platform.runLater(() -> { try { if (getModel().getConfig().getBoolean("stayOpenAfterLaunch")) { getModel().getStoryboard().showNormal(); } else { getModel().getStage().close(); } } catch (IOException e) { getModel().getStage().close(); } }); new Thread(() -> { Cleaner cleaner = new Cleaner(); cleaner.clean(); }).start(); } else { cancelLaunch(); Platform.runLater(() -> { progress.setProgress(1.0); progressDescription.setText(launcher.getErrorData()); }); } } } @Override public void resolvingFailed(@Nonnull Exception ex) { //noinspection ThrowableResultOfMethodCallIgnored if (getInnerExceptionOfType(SocketTimeoutException.class, ex) != null) { return; } Platform.runLater(() -> { progress.setProgress(1.0); progressDescription.setText(ex.getLocalizedMessage()); log.error("Resolving failed.", ex); }); } @Override public void updatedProgress(@Nonnull ProgressMonitor monitor) { progress.setProgress(monitor.getProgress()); } private void cancelLaunch() { updateLaunchButtons(true, false, false, false, false); } @FXML public void showOptions(@Nonnull ActionEvent actionEvent) { try { getModel().getStoryboard().showOptions(); } catch (@Nonnull IOException ignored) { } } }