This is an automated email from the ASF dual-hosted git repository. desruisseaux pushed a commit to branch geoapi-4.0 in repository https://gitbox.apache.org/repos/asf/sis.git
commit 54f5a9cb52ef58ca395a36711ef63c233767d93a Author: Martin Desruisseaux <[email protected]> AuthorDate: Thu Jan 28 00:50:11 2021 +0100 Provide an installation wizard for configuring the path to JavaFX. --- application/sis-javafx/src/main/artifact/bin/sisfx | 9 +- .../sis-javafx/src/main/artifact/bin/sisfx.bat | 2 +- .../org/apache/sis/internal/setup/FXFinder.java | 369 +++++++--- .../org/apache/sis/internal/setup/Inflater.java | 175 +++++ .../sis/internal/setup/LoggingConfiguration.java | 7 + .../java/org/apache/sis/internal/setup/Wizard.java | 759 +++++++++++++++++++++ .../org/apache/sis/internal/setup/WizardPage.java | 114 ++++ 7 files changed, 1334 insertions(+), 101 deletions(-) diff --git a/application/sis-javafx/src/main/artifact/bin/sisfx b/application/sis-javafx/src/main/artifact/bin/sisfx index 4f13a6b..fe4dfbe 100755 --- a/application/sis-javafx/src/main/artifact/bin/sisfx +++ b/application/sis-javafx/src/main/artifact/bin/sisfx @@ -18,10 +18,12 @@ set -o errexit -BASE_DIR="`readlink --canonicalize-existing $0`" -BASE_DIR="`dirname $BASE_DIR`/.." +BASE_DIR="`dirname $0`/.." source "$BASE_DIR/conf/setenv.sh" +SIS_DATA="${SIS_DATA:-$BASE_DIR/data}" +export SIS_DATA + if [ -z "$PATH_TO_FX" ] then java --class-path "$BASE_DIR/lib/*" org.apache.sis.internal.setup.FXFinder $BASE_DIR/conf/setenv.sh @@ -32,9 +34,6 @@ then source "$BASE_DIR/conf/setenv.sh" fi -SIS_DATA="${SIS_DATA:-$BASE_DIR/data}" -export SIS_DATA - # Execute SIS with any optional JAR that the user may put in the `lib` directory. java -splash:"$BASE_DIR/lib/logo.jpg" \ --add-modules javafx.graphics,javafx.controls \ diff --git a/application/sis-javafx/src/main/artifact/bin/sisfx.bat b/application/sis-javafx/src/main/artifact/bin/sisfx.bat index 92a5f0f..eb1126e 100644 --- a/application/sis-javafx/src/main/artifact/bin/sisfx.bat +++ b/application/sis-javafx/src/main/artifact/bin/sisfx.bat @@ -22,7 +22,7 @@ SET SIS_DATA=%BASE_DIR%\data IF "%PATH_TO_FX%"=="" ( java --class-path "%BASE_DIR%\lib\*" org.apache.sis.internal.setup.FXFinder "%BASE_DIR%\conf\setenv.bat" - if %ERRORLEVEL% GEQ 1 EXIT /B 1 + IF %ERRORLEVEL% GEQ 1 EXIT /B 1 CALL "%BASE_DIR%\conf\setenv.bat" ) diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/setup/FXFinder.java b/application/sis-javafx/src/main/java/org/apache/sis/internal/setup/FXFinder.java index 5cc2736..9184d56 100644 --- a/application/sis-javafx/src/main/java/org/apache/sis/internal/setup/FXFinder.java +++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/setup/FXFinder.java @@ -16,21 +16,17 @@ */ package org.apache.sis.internal.setup; -import java.awt.Desktop; -import java.awt.Font; import java.io.File; +import java.io.FileNotFoundException; import java.io.IOException; -import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.util.ArrayList; -import javax.swing.JFileChooser; -import javax.swing.JLabel; -import javax.swing.JOptionPane; -import javax.swing.UIManager; -import javax.swing.UnsupportedLookAndFeelException; +import java.util.Enumeration; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; /** @@ -44,25 +40,51 @@ import javax.swing.UnsupportedLookAndFeelException; */ public final class FXFinder { /** + * Minimal version of JavaFX required by Apache SIS. + */ + static final int JAVAFX_VERSION = 13; + + /** * The URL where to download JavaFX. */ - private static final String DOWNLOAD_URL = "https://openjfx.io/"; + static final String JAVAFX_HOME = "https://openjfx.io/"; + + /** + * Prefix of JavaFX directory. This is checked only in ZIP files. + * We do not check that name in decompressed directory because the + * user is free to rename. + */ + static final String JAVAFX_DIRECTORY_PREFIX = "javafx-sdk-"; /** * The {@value} directory in JavaFX installation directory. * This is the directory where JAR files are expected to be found. */ - private static final String LIB_DIRECTORY = "lib"; + private static final String JAVAFX_LIB_DIRECTORY = "lib"; + + /** + * A file to search in the {@value #JAVAFX_LIB_DIRECTORY} directory for determining if JavaFX is present. + */ + private static final String JAVAFX_SENTINEL_FILE = "javafx.controls.jar"; + + /** + * The environment variable containing the path to JavaFX {@value #JAVAFX_LIB_DIRECTORY} directory. + */ + static final String PATH_VARIABLE = "PATH_TO_FX"; /** - * A file to search in the {@value #LIB_DIRECTORY} directory for determining if JavaFX is present. + * The {@value} directory in Apache SIS installation where the {@code setenv.sh} file + * is expected to be located. */ - private static final String SENTINEL_FILE = "javafx.controls.jar"; + private static final String SIS_CONF_DIRECTORY = "conf"; /** - * The environment variable containing the path to JavaFX {@value #LIB_DIRECTORY} directory. + * The {@value} directory in Apache SIS installation where to unzip JavaFX. + * This is relative to {@code $BASE_DIR} environment variable. + * + * @see #decompress(Wizard) */ - private static final String PATH_VARIABLE = "PATH_TO_FX"; + private static final String SIS_UNZIP_DIRECTORY = "opt"; /** * File extension of Windows batch file. If the script file to edit does not have this extension, @@ -71,9 +93,47 @@ public final class FXFinder { private static final String WINDOWS_BATCH_EXTENSION = ".bat"; /** - * Do not allow instantiation of this class. + * Exit code to return when user cancelled the configuration process. + */ + private static final int CANCEL_EXIT_CODE = 1; + + /** + * Exit code to return if the wizard can not start. + */ + private static final int ERROR_EXIT_CODE = 2; + + /** + * The JavaFX directory as specified by the user, or {@code null} if none. + */ + private File specified; + + /** + * The JavaFX directory validated by {@code FXFinder}, or {@code null} if the directory is invalid. + * May be the same file than {@link #specified}, but not necessarily; it may be a subdirectory. */ - private FXFinder() { + private File validated; + + /** + * Path of the {@code setenv.sh} file to edit. + */ + private final Path setenv; + + /** + * The background task created if there is a JavaFX ZIP file to decompress. + */ + private Inflater inflater; + + /** + * {@code true} if this operation systems is Windows, or {@code false} if assumed Unix (Linux or MacOS). + */ + private final boolean isWindows; + + /** + * Creates a new finder. + */ + private FXFinder(final String setenv) { + this.setenv = Paths.get(setenv).normalize(); + isWindows = setenv.endsWith(WINDOWS_BATCH_EXTENSION); } /** @@ -82,110 +142,229 @@ public final class FXFinder { * @param args command line arguments. Should have a length of 1, * with {@code args[0]} containing the path of the file to edit. */ + @SuppressWarnings("UseOfSystemOutOrSystemErr") public static void main(String[] args) { - boolean success = false; - try { - success = askDirectory(Paths.get(args[0]).normalize()); - } catch (Exception e) { - JOptionPane.showMessageDialog(null, e.toString(), "Error", JOptionPane.ERROR_MESSAGE); + if (args.length == 1) { + if (Wizard.show(new FXFinder(args[0]))) { + // Call to `System.exit(int)` will be done by `Wizard`. + return; + } + } else { + System.out.println("Required: path to setenv.sh"); } - System.exit(success ? 0 : 1); + System.exit(ERROR_EXIT_CODE); } /** - * Popups a modal dialog box asking user to choose a directory. + * Returns {@code null} if the configuration file has been found and can be edited. + * If this method returns a non-null, then the setup wizard should be cancelled. + * The returned value can be used as an error message. + */ + final String diagnostic() { + if (Files.isReadable(setenv) && Files.isWritable(setenv)) { + return null; + } + return "Can not edit " + setenv; + } + + /** + * Returns the values of environment variables relevant to Apache SIS. + * This is used for showing a summary after configuration finished. + */ + final String[][] getEnvironmentVariables() { + return new String[][] { + getEnvironmentVariable("JAVA_HOME"), + getEnvironmentVariable(PATH_VARIABLE), + getEnvironmentVariable("SIS_DATA"), + getEnvironmentVariable("SIS_OPTS"), + }; + } + + /** + * Returns the value of the environment variable of given name. + * The returned array contains the following elements: * - * @param setenv path of the {@code setenv.sh} file to edit. - * @return {@code true} if we can continue with application launch, - * or {@code false} on error or cancellation. + * <ul> + * <li>Variable name</li> + * <li>Value to show (never null)</li> + * </ul> + */ + private String[] getEnvironmentVariable(final String name) { + String value; + try { + value = System.getenv(name); + if (value == null) { + value = "(undefined)"; + } else if (value.isEmpty()) { + value = "(blank)"; + } else if (name.equals("SIS_DATA") && value.equals("bin/../data")) { + value = Paths.get(value).toAbsolutePath().toString(); + } + } catch (SecurityException e) { + value = "(unreadable)"; + } + return new String[] {name, value}; + } + + /** + * Returns the name of JavaFX bundle to download, including the operating system name. + * Example: "JavaFX Linux SDK". This is for helping the user to choose which file to + * download on the {@value Constants#JAVAFX_HOME} web page. */ - private static boolean askDirectory(final Path setenv) throws Exception { + static String getJavafxBundleName() { + String name; try { - UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); - } catch (ReflectiveOperationException | UnsupportedLookAndFeelException e) { - // Ignore. + name = System.getProperty("os.name"); + } catch (SecurityException e) { + name = null; } - /* - * Checks now that we can edit `setenv.sh` content in order to not show the next - * dialog box if we czn not read that file (e.g. because the file was not found). - */ - if (!Files.isReadable(setenv) || !Files.isWritable(setenv)) { - JOptionPane.showMessageDialog(null, "Can not edit " + setenv, - "Configuration error", JOptionPane.WARNING_MESSAGE); - return false; + if (name == null) { + name = "<operating system>"; } - /* - * Ask the user what he wants to do. - */ - final JLabel description = new JLabel( - "<html><body><p style=\"width:400px; text-align:justify;\">" + - "This application requires <b>JavaFX</b> version 13 or later. " + - "Click on “Download” for opening the free download page. " + - "If JavaFX is already installed on this computer, " + - "click on “Set directory” for specifying the installation directory." + - "</p></body></html>"); - - description.setFont(description.getFont().deriveFont(Font.PLAIN)); - final Object[] options = {"Download", "Set directory", "Cancel"}; - final int choice = JOptionPane.showOptionDialog(null, description, - "JavaFX installation directory", - JOptionPane.YES_NO_CANCEL_OPTION, - JOptionPane.QUESTION_MESSAGE, - null, - options, - options[2]); - - if (choice == 0) { - if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) { - Desktop.getDesktop().browse(URI.create(DOWNLOAD_URL)); - } else { - JOptionPane.showMessageDialog(null, "See " + DOWNLOAD_URL, - "JavaFX download", JOptionPane.INFORMATION_MESSAGE); - } - } else if (choice == 1) { - final JFileChooser fd = new JFileChooser(); - fd.setDialogTitle("JavaFX installation directory"); - fd.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); - while (fd.showOpenDialog(null) == JFileChooser.APPROVE_OPTION) { - final File dir = findSubDirectory(fd.getSelectedFile()); - if (dir == null) { - JOptionPane.showMessageDialog(null, "Not a JavaFX directory.", - "JavaFX installation directory", JOptionPane.WARNING_MESSAGE); - } else { - setDirectory(setenv, dir); - return true; + return "JavaFX " + name + " SDK"; + } + + /** + * Returns the directory as validated by {@code FXFinder}. + * May be slightly different than the user-specified directory. + */ + final String getValidatedDirectory() { + return (validated != null) ? validated.getPath() : null; + } + + /** + * Returns the directory specified by the user, or {@code null} if none. + */ + final File getDirectory() { + return specified; + } + + /** + * Sets the JavaFX directory to the given value and checks its validity. + * This method tries to locate the {@code lib} sub-folder that we expect + * in a JavaFX installation directory. + * + * @param dir the directory from where to start the search. + * @return whether the given directory seems valid. + */ + final boolean setDirectory(final File dir) { + specified = dir; + validated = null; + if (new File(dir, JAVAFX_SENTINEL_FILE).exists()) { + validated = dir; + return true; + } + final File lib = new File(dir, JAVAFX_LIB_DIRECTORY); + if (new File(lib, JAVAFX_SENTINEL_FILE).exists()) { + validated = lib; + return true; + } + return false; + } + + /** + * Verifies whether the given file seems to be a valid ZIP file. + * This method checks for a sentinel value in the ZIP entries. + * The entry may be: + * + * <pre>javafx-sdk-<version>/lib/javafx.controls.jar</pre> + * + * If the file seems valid, {@code null} is returned. + * Otherwise an error message is HTML is returned. + */ + static String checkZip(final File file) throws IOException { + try (ZipFile zip = new ZipFile(file)) { + final Enumeration<? extends ZipEntry> entries = zip.entries(); + while (entries.hasMoreElements()) { + final ZipEntry entry = entries.nextElement(); + if (entry.isDirectory()) { + final String basedir = entry.getName(); + if (basedir.startsWith(JAVAFX_DIRECTORY_PREFIX)) { + final int start = JAVAFX_DIRECTORY_PREFIX.length(); + int end = basedir.indexOf('.', start); + if (end < start) end = basedir.length(); + final int version = Integer.parseInt(basedir.substring(start, end)); + if (version < JAVAFX_VERSION) { + return "<html>Apache SIS requires JavaFX version " + JAVAFX_VERSION + " or later. " + + "The given file contains JavaFX version " + version + ".</html>"; + } + if (zip.getEntry(basedir + JAVAFX_LIB_DIRECTORY + '/' + JAVAFX_SENTINEL_FILE) != null) { + return null; // Valid file. + } + } + break; } } } - return false; + return "<html>Not a recognized ZIP file for JavaFX SDK.</html>"; } /** - * Tries to locate the {@code lib} sub-folder in a JavaFX installation directory. + * Returns the destination directory where to decompress ZIP files. + * This method assumes the following directory structure: * - * @param dir the directory from where to start the search. - * @return the {@code lib} directory, or {@code null} if not found. + * {@preformat text + * apache-sis (can be any name) + * ├─ conf + * │ └─ setenv.sh + * └─ opt + * } */ - private static File findSubDirectory(final File dir) { - if (new File(dir, SENTINEL_FILE).exists()) { - return dir; + final File getDestinationDirectory() throws IOException { + File basedir = setenv.toAbsolutePath().toFile().getParentFile(); + if (basedir != null && SIS_CONF_DIRECTORY.equals(basedir.getName())) { + basedir = basedir.getParentFile(); + if (basedir != null) { + final File destination = new File(basedir, SIS_UNZIP_DIRECTORY); + if (destination.isDirectory() || destination.mkdir()) { + return destination; + } + throw new IOException("Can not create directory: " + destination); + } } - final File lib = new File(dir, LIB_DIRECTORY); - if (new File(lib, SENTINEL_FILE).exists()) { - return lib; + throw new FileNotFoundException("No parent directory to " + setenv + '.'); + } + + /** + * If the user-specified file is a ZIP file, starts decompression in a background thread. + * + * @return whether decompression started. + */ + final boolean decompress(final Wizard wizard) { + if (validated == null) { + inflater = new Inflater(wizard, specified); + final Thread t = new Thread(inflater, "Inflater"); + t.start(); + return true; } - return null; + return false; } /** - * Sets the JavaFX directory. + * Cancels configuration, deletes decompressed files if any and exits. + * This method is invoked by {@link Wizard} in the following situations: + * + * <ul> + * <li>User clicked on the "Cancel" button.</li> + * <li>User clicked on the "Close window" button in window title bar.</li> + * </ul> * - * @param setenv path to the {@code setenv.sh} file to edit. - * @param dir directory selected by user. + * If a decompression is in progress, it is stopped and all files are deleted. + */ + final void cancel() { + if (inflater != null) { + inflater.cancel(); + } + System.exit(CANCEL_EXIT_CODE); + } + + /** + * Commits the configuration by writing the JavaFX directory in the {@code setenv.sh} file. */ - private static void setDirectory(final Path setenv, final File dir) throws IOException { + final void commit() throws IOException { + inflater = null; String command = PATH_VARIABLE; - if (setenv.getFileName().toString().endsWith(WINDOWS_BATCH_EXTENSION)) { + if (isWindows) { command = "SET " + command; // Microsoft Windows syntax. } final ArrayList<String> content = new ArrayList<>(); @@ -201,7 +380,7 @@ public final class FXFinder { if (insertAt < 0) { insertAt = content.size(); } - content.add(insertAt, command + '=' + dir); + content.add(insertAt, command + '=' + validated); Files.write(setenv, content, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING); } } diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/setup/Inflater.java b/application/sis-javafx/src/main/java/org/apache/sis/internal/setup/Inflater.java new file mode 100644 index 0000000..5e680f8 --- /dev/null +++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/setup/Inflater.java @@ -0,0 +1,175 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sis.internal.setup; + +import java.awt.EventQueue; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Enumeration; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import javax.swing.JProgressBar; + + +/** + * Decompress the ZIP file for JavaFX in a background thread. + * + * <p><b>Design note:</b> we do not use {@link javax.swing.SwingWorker} because that classes + * is more expansive than what we need. For example it creates a pool of 10 threads while we + * need only one.</p> + * + * @author Martin Desruisseaux (Geomatys) + * @version 1.1 + * @since 1.1 + * @module + */ +final class Inflater implements Runnable { + /** + * The wizard to notify about completion of failure. + */ + private final Wizard wizard; + + /** + * The zip file to decompress. + */ + private final File source; + + /** + * The directory where the ZIP file is decompressed. + */ + private File destination; + + /** + * The {@linkplain #destination} directory, plus the first subdirectory in + * the ZIP file that starts with {@value FXFinder#JAVAFX_DIRECTORY_PREFIX}. + */ + private File subdir; + + /** + * If decompression failed, the cause. Otherwise {@code null}. + */ + private Exception failure; + + /** + * Whether this task is cancelled. + */ + private volatile boolean cancelled; + + /** + * Creates a new inflater for the specified ZIP file. + */ + Inflater(final Wizard wizard, final File source) { + this.wizard = wizard; + this.source = source; + } + + /** + * The task to be executed in a background {@link Thread}. + * This method dispatches the work to {@link #doInBackground()} and {@link #done()} methods. + */ + @Override + public synchronized void run() { + try { + doInBackground(); + } catch (Exception e) { + failure = e; + delete(destination); + } + EventQueue.invokeLater(this::done); + } + + /** + * Decompresses the JavaFX ZIP file. + */ + private void doInBackground() throws Exception { + destination = wizard.javafxFinder.getDestinationDirectory(); + final JProgressBar progressBar = wizard.inflateProgress; + final byte[] buffer = new byte[65536]; + try (ZipFile zip = new ZipFile(source)) { + final int size = zip.size(); + EventQueue.invokeAndWait(() -> progressBar.setMaximum(size)); + final Enumeration<? extends ZipEntry> entries = zip.entries(); + int progressValue = 0; + while (entries.hasMoreElements()) { + final ZipEntry entry = entries.nextElement(); + final File file = new File(destination, entry.getName()); + if (entry.isDirectory()) { + if (!file.isDirectory() && !file.mkdir()) { + throw new IOException("Directory can not be created: " + file); + } + if (subdir == null && entry.getName().startsWith(FXFinder.JAVAFX_DIRECTORY_PREFIX)) { + subdir = file; + } + } else { + try (InputStream in = zip.getInputStream(entry); + OutputStream out = new FileOutputStream(file)) // No need for buffered streams here. + { + int n; + while ((n = in.read(buffer)) >= 0) { + if (cancelled) return; + out.write(buffer, 0, n); + } + } + } + final int p = progressValue++; + EventQueue.invokeLater(() -> progressBar.setValue(p)); + } + } + } + + /** + * Invoked in Swing thread after the decompression is done, either successfully or on failure. + * Note that {@link #cancelled} may be {@code true} only if {@link #cancel()} has been invoked, + * in which case this method does nothing because the files will be deleted by {@code cancel()} + * and the system will exit. + */ + private void done() { + if (!cancelled) { + if (subdir == null) subdir = destination; + wizard.decompressionFinished(subdir, failure); + } + } + + /** + * Stops the thread if it is running, then delete the files. + * This method is invoked by {@link FXFinder} just before {@link System#exit(int)}. + */ + final void cancel() { + cancelled = true; + synchronized (this) { // Wait for background thread to finish. + delete(destination); + } + } + + /** + * Deletes a directory and all its content recursively. + */ + private static void delete(final File directory) { + if (directory != null) { + final File[] content = directory.listFiles(); + if (content != null) { + for (final File file : content) { + delete(file); + } + } + directory.delete(); + } + } +} diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/setup/LoggingConfiguration.java b/application/sis-javafx/src/main/java/org/apache/sis/internal/setup/LoggingConfiguration.java index 0af0f66..b03a302 100644 --- a/application/sis-javafx/src/main/java/org/apache/sis/internal/setup/LoggingConfiguration.java +++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/setup/LoggingConfiguration.java @@ -33,6 +33,13 @@ import java.nio.file.Paths; * <p>This class should not use any SIS classes because it may be invoked early * while the application is still initializing.</p> * + * <p>This class is not referenced directly by other Java code. Instead, it is + * specified at JVM startup time like below:</p> + * + * {@preformat shell + * java -Djava.util.logging.config.class="org.apache.sis.internal.setup.LoggingConfiguration" + * } + * * @author Martin Desruisseaux (Geomatys) * @version 1.1 * @since 1.1 diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/setup/Wizard.java b/application/sis-javafx/src/main/java/org/apache/sis/internal/setup/Wizard.java new file mode 100644 index 0000000..6ef160e --- /dev/null +++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/setup/Wizard.java @@ -0,0 +1,759 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sis.internal.setup; + +import java.awt.BorderLayout; +import java.awt.CardLayout; +import java.awt.Color; +import java.awt.Component; +import java.awt.Container; +import java.awt.Desktop; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.UnsupportedFlavorException; +import java.awt.dnd.DnDConstants; +import java.awt.dnd.DropTarget; +import java.awt.dnd.DropTargetDragEvent; +import java.awt.dnd.DropTargetDropEvent; +import java.awt.dnd.DropTargetEvent; +import java.awt.dnd.DropTargetListener; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import javax.swing.Box; +import javax.swing.JButton; +import javax.swing.JFileChooser; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JProgressBar; +import javax.swing.JSeparator; +import javax.swing.UIManager; +import javax.swing.UnsupportedLookAndFeelException; +import javax.swing.border.Border; +import javax.swing.border.EmptyBorder; +import javax.swing.border.LineBorder; +import javax.swing.filechooser.FileFilter; + + +/** + * Configuration wizard for Apache SIS. + * The wizard contains the following step: + * + * <ul> + * <li>Introduction</li> + * <li>Internet page where to download JavaFX.</li> + * <li>Path to JavaFX installation directory.</li> + * <li>Configuration summary</li> + * </ul> + * + * This class provides all the Graphical User Interface (GUI) using Swing widgets. + * The class doing actual work for managing SIS configuration is {@link FXFinder}. + * + * @author Martin Desruisseaux (Geomatys) + * @version 1.1 + * @since 1.1 + * @module + */ +final class Wizard extends FileFilter implements ActionListener, PropertyChangeListener, DropTargetListener { + /** + * Initializes Look and Feel before to construct any Swing component. + */ + static { + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } catch (ReflectiveOperationException | UnsupportedLookAndFeelException e) { + // Ignore. + } + UIManager.put("FileChooser.readOnly", Boolean.TRUE); + } + + /** + * The window width, in pixels. + */ + private static final int WIDTH = 700; + + /** + * Label of button to shown in the wizard, also used as action identifier. + */ + private static final String BACK = "Back", NEXT = "Next", CANCEL = "Cancel", + JAVAFX_HOME = "Open JavaFX home page", BROWSE = "Browse", SELECT = "Select"; + + /** + * Color of {@linkplain #titles} for pages other than the current page. + * + * Conceptually a {@code static final} constant, but declared non-static for initializing + * it only at {@link Wizard} creation time and because that creation will happen only once. + */ + private final Color TITLE_COLOR = new Color(36, 113, 163); + + /** + * Color of title for the {@linkplain #currentPage current page}. + * + * Conceptually a {@code static final} constant, but declared non-static for initializing + * it only at {@link Wizard} creation time and because that creation will happen only once. + */ + private final Color SELECTED_TITLE_COLOR = new Color(21, 67, 96); + + /** + * Bullet in front of (selected) titles. Bullets should have the same width + * for avoiding change in {@link #titles} text position when user move from + * one page to the other. + */ + private static final String TITLE_BULLET = "• ", SELECTED_TITLE_BULLET = "‣ "; + + /** + * The normal {@link #javafxPath} border. + * + * Conceptually a {@code static final} constant, but declared non-static for initializing + * it only at {@link Wizard} creation time and because that creation will happen only once. + */ + private final Border JAVAFX_PATH_BORDER = new LineBorder(Color.GRAY); + + /** + * The {@link #javafxPath} border during drag and drop action. We use a green border. + * + * Conceptually a {@code static final} constant, but declared non-static for initializing + * it only at {@link Wizard} creation time and because that creation will happen only once. + */ + private final Border JAVAFX_PATH_BORDER_DND = new LineBorder(new Color(40, 180, 99), 3); + + /** + * The top-level window where wizard will be shown. + */ + private final JFrame wizard; + + /** + * The panel where each wizard step is shown. This panel uses a {@link CardLayout}. + */ + private final JPanel cardPanel; + + /** + * The button for moving to next page. + * Its label will be changed from "Next" to "Finish" when on the last page. + */ + private final JButton nextButton; + + /** + * The button for moving to previous page. + * Disabled when on the first page. + */ + private final JButton backButton; + + /** + * The button for cancelling setup. + * Disabled when on the last page. + */ + private final JButton cancelButton; + + /** + * The button for selecting a directory or a ZIP file. This button may be + * non-null only during the time that a {@link JFileChooser} is visible. + * + * @see #findSelectButton(Container) + */ + private JButton selectButton; + + /** + * Titles of each page. This is highlighted during navigation. + */ + private final JLabel[] titles; + + /** + * The page currently shown. + */ + private WizardPage currentPage; + + /** + * Whether this wizard accepts the JavaFX location specified in the {@link WizardPage#JAVAFX_LOCATION} page. + */ + private boolean acceptLocation; + + /** + * JavaFX directory or ZIP file. + * + * @see #setJavafxPath(File) + */ + final FXFinder javafxFinder; + + /** + * View of the path to JavaFX installation directory. This is the value of {@link FXFinder#getDirectory()}, + * potentially shown in red if the location is not valid. + */ + private final JLabel javafxPath; + + /** + * If the {@link #javafxPath} is not valid, a message for the user. Otherwise this label is empty. + */ + private final JLabel javafxPathError; + + /** + * The message shown on the last page. This is <cite>"Apache SIS setup is completed"</cite>, + * but may be changed if the setup failed. + */ + private JLabel finalMessage; + + /** + * Final value of JavaFX path shown in the last page. May be slightly different than {@link #javafxPath}. + */ + private JLabel finalJavafxPath; + + /** + * Information about progress of decompression process. + */ + final JProgressBar inflateProgress; + + /** + * Creates a new wizard. + * + * @see #show(FXFinder) + */ + private Wizard(final FXFinder javafxFinder) { + this.javafxFinder = javafxFinder; + wizard = new JFrame("Apache SIS setup"); + final Container content = wizard.getContentPane(); + content.setLayout(new BorderLayout()); + /* + * Back, Next, Cancel button. + */ + { // For keeping variables in a local scope. + final Box buttons = Box.createHorizontalBox(); + buttons.setBorder(new EmptyBorder(9, 12, 9, 15)); // Top, left, bottom, right. + backButton = createButton(buttons, BACK); buttons.add(Box.createHorizontalStrut(10)); + nextButton = createButton(buttons, NEXT); buttons.add(Box.createHorizontalStrut(30)); + cancelButton = createButton(buttons, CANCEL); + backButton.setEnabled(false); + + final JPanel bottom = new JPanel(new BorderLayout()); + bottom.add(new JSeparator(), BorderLayout.NORTH); + bottom.add(buttons, java.awt.BorderLayout.EAST); + content.add(bottom, BorderLayout.SOUTH); + } + /* + * Navigation panel on the left side with the following titles + * (currently shown page is highlighted): + * + * - Introduction + * - Download + * - Set directory + * - Summary + */ + final WizardPage[] pages = WizardPage.values(); + { + titles = new JLabel[pages.length]; + final EmptyBorder padding = new EmptyBorder(3, 0, 3, 0); + final Box summary = Box.createVerticalBox(); + for (int i=0; i<pages.length; i++) { + final String title = (i == 0 ? SELECTED_TITLE_BULLET : TITLE_BULLET) + pages[i].title; + final JLabel label = new JLabel(title, JLabel.LEFT); + label.setForeground(i == 0 ? SELECTED_TITLE_COLOR : TITLE_COLOR); + label.setBorder(padding); + summary.add(titles[i] = label); + } + final JPanel pane = new JPanel(); + pane.setBackground(new Color(169, 204, 227)); + pane.setBorder(new EmptyBorder(40, 15, 9, 24)); // Top, left, bottom, right. + pane.add(summary); + content.add(pane, BorderLayout.WEST); + } + /* + * The main content where text is shown, together with download button, directory chooser, etc. + * The content of each page is created by `createPage(…)`. They all have in common to start with + * a description text formatted in HTML. + */ + { + final Font font = new Font(Font.SERIF, Font.PLAIN, 14); + javafxPath = new JLabel(); + javafxPath.setBorder(JAVAFX_PATH_BORDER); + javafxPathError = new JLabel(); + javafxPathError.setForeground(Color.RED); + javafxPathError.setFont(font); + inflateProgress = new JProgressBar(); + cardPanel = new JPanel(new CardLayout()); + cardPanel.setBorder(new EmptyBorder(30, 30, 9, 30)); // Top, left, bottom, right. + cardPanel.setBackground(Color.WHITE); + for (final WizardPage page : pages) { + cardPanel.add(createPage(page, font), page.name()); + // The initially visible component is the first added. + } + currentPage = pages[0]; + content.add(cardPanel, BorderLayout.CENTER); + } + wizard.setSize(WIDTH, 500); // Must be before `setLocationRelativeTo(…)`. + wizard.setResizable(false); + wizard.setLocationRelativeTo(null); + wizard.addWindowListener(new WindowAdapter() { + @Override public void windowClosing(WindowEvent event) { + javafxFinder.cancel(); + } + }); + } + + /** + * Invoked by the constructor for preparing in advance each page in a {@link CardLayout}. + * Each page starts with a text formatted in HTML using a Serif font (such as Times), + * followed by control specific to each page. + * + * @param page identifies the page to create. + * @param font Serif font to use for the text. + */ + private Box createPage(final WizardPage page, final Font font) { + final Box content = Box.createVerticalBox(); + final JLabel text = new JLabel(page.text, JLabel.LEFT); + text.setFont(font); + content.add(text); + content.add(Box.createVerticalStrut(30)); + switch (page) { + case DOWNLOAD_JAVAFX: { + createButton(content, JAVAFX_HOME).setToolTipText(FXFinder.JAVAFX_HOME); + final JLabel instruction = new JLabel(WizardPage.downloadSteps()); + instruction.setFont(font.deriveFont(12f)); + content.add(instruction); + break; + } + case JAVAFX_LOCATION: { + javafxPath.setMinimumSize(new Dimension( 100, 30)); + javafxPath.setMaximumSize(new Dimension(WIDTH, 30)); + content.add(javafxPath); + content.add(Box.createVerticalStrut(12)); + createButton(content, BROWSE); + content.add(Box.createVerticalStrut(24)); + content.add(javafxPathError); + content.setDropTarget(new DropTarget(content, this)); + break; + } + case DECOMPRESS: { + inflateProgress.setMinimumSize(new Dimension( 100, 21)); + inflateProgress.setMaximumSize(new Dimension(WIDTH, 21)); + content.add(inflateProgress); + break; + } + case COMPLETED: { + finalMessage = text; + final Border vb = new EmptyBorder(0, 15, 9, 0); + final Font fn = new Font(Font.MONOSPACED, Font.BOLD, 13); + final Font fv = new Font(Font.SANS_SERIF, Font.PLAIN, 13); + for (final String[] variable : javafxFinder.getEnvironmentVariables()) { + final JLabel name = new JLabel(variable[0] + ':'); + final JLabel value = new JLabel(variable[1]); + name .setForeground(Color.DARK_GRAY); + value.setForeground(Color.DARK_GRAY); + name .setFont(fn); + value.setFont(fv); + value.setBorder(vb); + name.setLabelFor(value); + content.add(name); + content.add(value); + if (FXFinder.PATH_VARIABLE.equals(variable[0])) { + finalJavafxPath = value; + } + } + break; + } + } + return content; + } + + /** + * Creates a button and adds it to the given box. A listener is registered + * for an action having the same name than the button label. + * + * @param addTo the horizontal box where to add the button. + * @param label button labels, also used as action identifier. + * @return the added button. + */ + private JButton createButton(final Box addTo, final String label) { + JButton button = new JButton(label); + button.setActionCommand(label); + button.addActionListener(this); + addTo.add(button); + return button; + } + + /** + * Invoked when user clicks on a button. + * The action name is the label given to {@link #createButton(Box, String)}. + */ + @Override + public void actionPerformed(final ActionEvent event) { + switch (event.getActionCommand()) { + case CANCEL: javafxFinder.cancel(); break; + case BACK: nextOrPreviousPage(-1); break; + case NEXT: nextOrPreviousPage(+1); break; + case JAVAFX_HOME: openJavafxHomePage(); break; + case BROWSE: showDirectoryChooser(); break; + } + } + + /** + * Invoked when the user clicks on the {@value #BACK} or {@value #NEXT} button for moving to the + * previous page or to the next page. This method changes the highlighted title on the left side, + * updates the buttons enabled status and shows the new page. + * + * <p>Moving to the next page may cause the following actions:</p> + * <ul> + * <li>Moving to the last page cause a call to {@link FXFinder#commit()}.</li> + * <li>Moving after the last page cause a system exit (wizard finished).</li> + * </ul> + * + * @param n -1 for previous page, or +1 for next page. + */ + private void nextOrPreviousPage(final int n) { + /* + * Restore title (on the left side) of current page to default color. + * In other words, remove highlighting. + */ + int index = currentPage.ordinal(); + JLabel title = titles[index]; + title.setForeground(TITLE_COLOR); + title.setText(TITLE_BULLET + currentPage.title); + final WizardPage[] pages = WizardPage.values(); + if ((index += n) >= pages.length) { + /* + * User clicked on "Finish" in the last page: + * wizard finished successfully. + */ + wizard.dispose(); + System.exit(0); + return; + } + /* + * Highlight title (on the left side) of new current page. + * Next, there is some specific actions depending on the new page. + */ + currentPage = pages[index]; + title = titles[index]; + title.setForeground(SELECTED_TITLE_COLOR); + title.setText(SELECTED_TITLE_BULLET + currentPage.title); + backButton.setEnabled(index > 0); + nextButton.setEnabled(true); + switch (currentPage) { + case JAVAFX_LOCATION: { + nextButton.setEnabled(acceptLocation); + break; + } + case DECOMPRESS: { + backButton.setEnabled(false); + nextButton.setEnabled(false); + if (!javafxFinder.decompress(this)) { + nextOrPreviousPage(n); // Nothing to decompress, skip this page. + return; + } + break; + } + case COMPLETED: { + backButton.setEnabled(false); + nextButton.setText("Finish"); + try { + javafxFinder.commit(); + cancelButton.setEnabled(false); + finalJavafxPath.setText(javafxFinder.getValidatedDirectory()); + } catch (IOException e) { + nextButton.setEnabled(false); + finalMessage.setForeground(Color.RED); + finalMessage.setText(getHtmlMessage("Apache SIS setup can not be completed.", e)); + } + break; + } + } + ((CardLayout) cardPanel.getLayout()).show(cardPanel, currentPage.name()); + } + + /** + * Invoked in Swing thread after decompression finished either successfully or on failure. + * Note that there is no method for cancelled operation because in such case, + * {@link FXFinder#cancel()} will be invoked directly. + * + * @param destination the directory where ZIP files have been decompressed. + * @param failure if decompression failed, the error. Otherwise {@code null}. + */ + final void decompressionFinished(final File destination, final Exception failure) { + final boolean isValid; + if (failure != null) { + isValid = false; + javafxPathError.setText(getHtmlMessage("Can not decompress the file.", failure)); + } else { + isValid = setJavafxPath(destination); + } + nextOrPreviousPage(isValid ? +1 : -1); + } + + /** + * Returns a non-null message for given exception with HTML characters escaped. + */ + private static String getHtmlMessage(final String header, final Exception e) { + final StringBuilder buffer = new StringBuilder(100).append("<html>").append(header) + .append("<br><b>").append(e.getClass().getSimpleName()).append("</b>"); + String message = e.getLocalizedMessage(); + if (message != null) { + message = message.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """); + buffer.append(": ").append(message); + } + return buffer.append("</html>").toString(); + } + + /** + * Invoked when the user clicks on the {@value #JAVAFX_HOME} button + * for opening the {@value Constants#JAVAFX_HOME} URL in a browser. + */ + private void openJavafxHomePage() { + if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) try { + Desktop.getDesktop().browse(new URI(FXFinder.JAVAFX_HOME)); + nextOrPreviousPage(1); + } catch (URISyntaxException | IOException e) { + JOptionPane.showMessageDialog(wizard, e.toString(), "Error", JOptionPane.ERROR_MESSAGE); + } else { + JOptionPane.showMessageDialog(wizard, "Can not find internet browser on this computer.\n" + + "See " + FXFinder.JAVAFX_HOME + " for download information.", + "JavaFX download", JOptionPane.INFORMATION_MESSAGE); + } + } + + /** + * Invoked when user clicks on the {@value #BROWSE} button for choosing a JavaFX installation directory. + * If user selects an invalid file or directory, the chooser popups again until the user selects a valid + * file or cancels. + */ + private void showDirectoryChooser() { + final JFileChooser fd = new JFileChooser(javafxFinder.getDirectory()); + fd.addChoosableFileFilter(this); + fd.setFileFilter(this); + fd.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES); + fd.setDialogTitle("JavaFX installation directory"); + fd.setApproveButtonText(SELECT); + selectButton = findSelectButton(fd); + fd.setApproveButtonText(null); + if (selectButton != null) { + selectButton.setEnabled(false); + fd.addPropertyChangeListener(this); + } + if (fd.showOpenDialog(wizard) == JFileChooser.APPROVE_OPTION) { + setJavafxPath(fd.getSelectedFile()); + } + selectButton = null; + } + + /** + * Searches recursively for the {@value #SELECT} button in the given container. This is used for + * locating the "Open" button in {@link JFileChooser}. Caller needs to temporarily change button + * text to {@value #SELECT} before to invoke this method. We can not search directly for "Open" + * text because that text may be localized. + * + * @param c the container where to search for the {@value #SELECT} button. + */ + private static JButton findSelectButton(final Container c) { + final int n = c.getComponentCount(); + for (int i=0; i<n; i++) { + final Component child = c.getComponent(i); + if (child instanceof JButton) { + final JButton button = (JButton) child; + if (SELECT.equals(button.getText())) { + return button; + } + } else if (child instanceof Container) { + final JButton button = findSelectButton((Container) child); + if (button != null) return button; + } + } + return null; + } + + /** + * Returns the description to show in {@link JFileChooser} for possible JavaFX installation files. + * The list of accepted file formats includes ZIP files. + * + * @return description of this filter to show in file chooser. + */ + @Override + public String getDescription() { + return "ZIP files"; + } + + /** + * Returns whether the given file is shown in {@link JFileChooser} as a possible JavaFX installation file. + * This method performs a cheap test based on the extension. + * + * @param file the file to test. + * @return whether the given file should be shown in the file chooser. + */ + @Override + public boolean accept(final File file) { + if (file.isDirectory()) { + return true; + } + final String name = file.getName(); + final int s = name.lastIndexOf('.'); + return (s >= 0) && name.regionMatches(true, s+1, "zip", 0, 3); + } + + /** + * Invoked when a {@link JFileChooser} property changed. If the property change tells us that + * file selection changed (including the case where user changed directory), then this method + * checks if the new selection is valid. This determines whether {@value #NEXT} button should + * be enabled. + * + * @param event a description of the change. + */ + @Override + @SuppressWarnings("CallToPrintStackTrace") + public void propertyChange(final PropertyChangeEvent event) { + final File file; + switch (event.getPropertyName()) { + default: { + return; + } + case JFileChooser.SELECTED_FILE_CHANGED_PROPERTY: { + file = (File) event.getNewValue(); + break; + } + case JFileChooser.DIRECTORY_CHANGED_PROPERTY: { + file = ((JFileChooser) event.getSource()).getSelectedFile(); + break; + } + } + /* + * Perform a cheap validity check (without opening the file) because this method may + * be invoked often while user navigates through files. A more extensive check will + * be done later by `setJavafxPath(File)`. + */ + boolean enabled = false; + if (file != null) { + if (file.isFile()) { + enabled = true; + } else if (file.isDirectory()) { + enabled = javafxFinder.setDirectory(file); + } + } + selectButton.setEnabled(enabled); + } + + /** + * Sets the JavaFX directory and enables or disables the {@value #NEXT} button depending + * on whether that file or directory is valid. If the file is not valid, a message will + * be set in {@link #javafxPathError} (below the path). + * + * @param dir the JavaFX directory or ZIP file, or {@code null} if none. + * @return whether the given file or directory is valid. + */ + private boolean setJavafxPath(final File dir) { + String error = null; + boolean isValid = javafxFinder.setDirectory(dir); + if (!isValid) { + if (dir.isFile()) try { + error = FXFinder.checkZip(dir); + isValid = (error == null); + } catch (IOException e) { + error = getHtmlMessage("Can not open the file.", e); + } else { + error = "<html>Not a recognized JavaFX directory or ZIP file.</html>"; + } + } + javafxPath.setText(dir != null ? dir.getPath() : null); + javafxPath.setForeground(isValid ? Color.DARK_GRAY : Color.RED); + if (currentPage == WizardPage.JAVAFX_LOCATION) { + nextButton.setEnabled(isValid); + } + javafxPathError.setText(error); + acceptLocation = isValid; + return isValid; + } + + /** + * Invoked when user drops files in the wizard. This is an alternative way to set the JavaFX directory. + * + * @param event the drop event. + */ + @Override + @SuppressWarnings("unchecked") + public void drop(final DropTargetDropEvent event) { + for (final DataFlavor flavor : event.getCurrentDataFlavors()) { + if (flavor.isFlavorJavaFileListType()) { + javafxPath.setBorder(JAVAFX_PATH_BORDER); + event.acceptDrop(DnDConstants.ACTION_LINK); + try { + for (final File file : (Iterable<File>) event.getTransferable().getTransferData(DataFlavor.javaFileListFlavor)) { + if (setJavafxPath(file)) break; + } + } catch (UnsupportedFlavorException | IOException e) { + javafxPathError.setText(getHtmlMessage("Can not open the file.", e)); + } + event.dropComplete(true); + return; + } + } + event.rejectDrop(); + } + + /** + * Invoked when the user is doing a drag and drop action and is entering in the target area. + * This method sets a visual hint for telling to the user that the wizard is ready to receives the files. + */ + @Override + public void dragEnter(final DropTargetDragEvent event) { + for (final DataFlavor flavor : event.getCurrentDataFlavors()) { + if (flavor.isFlavorJavaFileListType()) { + javafxPath.setBorder(JAVAFX_PATH_BORDER_DND); + break; + } + } + } + + /** + * Invoked when the user is doing a drag and drop action and is exiting in the target area. + * This method cancels the visual hint created by {@link #dragEnter(DropTargetDragEvent)}. + */ + @Override + public void dragExit(final DropTargetEvent event) { + javafxPath.setBorder(JAVAFX_PATH_BORDER); + } + + /** Ignored. */ + @Override public void dragOver(final DropTargetDragEvent event) {} + + /** Ignored. */ + @Override public void dropActionChanged(final DropTargetDragEvent event) {} + + /** + * Shows the installation wizard. + * + * @return {@code true} if the wizard has been started, or {@code false} on configuration error. + */ + public static boolean show(final FXFinder javafxFinder) { + /* + * Checks now that we can edit `setenv.sh` content in order to not show the wizard + * if we can not read that file (e.g. because the file was not found). + */ + final String diagnostic = javafxFinder.diagnostic(); + if (diagnostic != null) { + JOptionPane.showMessageDialog(null, diagnostic, "Configuration error", JOptionPane.ERROR); + return false; + } else { + final Wizard wizard = new Wizard(javafxFinder); + wizard.wizard.setVisible(true); + return true; + } + } +} diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/setup/WizardPage.java b/application/sis-javafx/src/main/java/org/apache/sis/internal/setup/WizardPage.java new file mode 100644 index 0000000..29cf5ef --- /dev/null +++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/setup/WizardPage.java @@ -0,0 +1,114 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sis.internal.setup; + + +/** + * An identifier of the page to shown in {@link Wizard}. + * Pages are shown in enumeration order. + * + * @author Martin Desruisseaux (Geomatys) + * @version 1.1 + * @since 1.1 + * @module + */ +enum WizardPage { + /** + * Plain text saying what this wizard will do. + */ + INTRODUCTION("Introduction", + "<html><h1>Welcome to Apache SIS™</h1>" + + "<p>" + + "This wizard will configure Apache Spatial Information System (SIS) " + + "JavaFX application on your computer. " + + "This configuration needs to be done only once. " + + "Click <u>Next</u> to continue, or <u>Cancel</u> to exit setup." + + "</p><p style=\"padding-top:20px; font-size:10px; color:#909090;\">" + + "Apache SIS is licensed under the Apache License, version 2.0." + + "</p></html>"), + + /** + * Page proposing to download JavaFX, with a "Download" button. + * Those instructions a completed by {@link #downloadSteps()}. + */ + DOWNLOAD_JAVAFX("Download", + "<html><p style=\"padding-top:10px;\">" + + "This application requires <i>JavaFX</i> (or <i>OpenJFX</i>) version " + FXFinder.JAVAFX_VERSION + " or later. " + + "OpenJFX is free software, licensed under the GPL with the class path exception. " + + "Click on <u>Open JavaFX home page</u> for opening the JavaFX home page. " + + "If JavaFX or OpenJFX has already been downloaded on this computer, " + + "skip download and click on <u>Next</u> for specifying its installation directory." + + "</p></html>"), + + /** + * Page asking to specify the installation directory. + */ + JAVAFX_LOCATION("JavaFX location", + "<html><p style=\"padding-top:10px;\">" + + "Specify the downloaded ZIP file, or the directory where JavaFX or OpenJFX has been installed. " + + "You can drag and drop the file or directory below or click on <u>Browse</u>." + + "</p></html>"), + + /** + * Page notifying user that a decompression is in progress. + * This page is skipped if the user specified an existing directory instead than a ZIP file. + */ + DECOMPRESS("Decompress", + "<html><p style=\"padding-top:10px;\">" + + "Decompressing ZIP file." + + "</p></html>"), + + /** + * Final page saying that the configuration is completed. + */ + COMPLETED("Summary", + "<html><p style=\"padding-top:10px;\">" + + "Apache SIS setup is completed. " + + "Environment variables relevant to SIS are listed below." + + "</p></html>"); + + /** + * Complement to {@link #DOWNLOAD_JAVAFX}. + */ + static String downloadSteps() { + return "<html><ul>" + + "<li>Click on <b>Download</b>.</li>" + + "<li>Scroll down to <b>Latest releases</b>.</li>" + + "<li>Download <b>" + FXFinder.getJavafxBundleName() + "</b>.</li>" + + "<li><em>(Optional)</em> decompress the ZIP file in any directory.</li>" + + "<li>Click <u>Next</u> to continue.</li>" + + "</ul></html>"; + } + + /** + * Title for this page. + */ + final String title; + + /** + * The text to shown on the page. + */ + final String text; + + /** + * Creates a new enumeration for a page showing the specified text. + */ + private WizardPage(final String title, final String text) { + this.title = title; + this.text = text; + } +}
