Skip to content

Commit

Permalink
feat: Persist GUI app settings between executions (#1169)
Browse files Browse the repository at this point in the history
Currently, the GUI app does not remember settings (input and output paths, advanced settings) between different executions of the app. This PR adds support for persisting settings between runs of the validator, using the Java Preferences API for storage.

Closes #1165.
  • Loading branch information
bdferris-v2 authored May 27, 2022
1 parent 3ee0051 commit 75c64a9
Show file tree
Hide file tree
Showing 6 changed files with 192 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.ResourceBundle;
import javax.swing.BorderFactory;
import javax.swing.Box;
Expand Down Expand Up @@ -68,6 +70,8 @@ public class GtfsValidatorApp extends JFrame {
private final ValidationDisplay validationDisplay;
private final ResourceBundle bundle;

private final List<Runnable> preValidationCallbacks = new ArrayList<>();

public GtfsValidatorApp(
MonitoredValidationRunner validationRunner, ValidationDisplay validationDisplay) {
super("GTFS Schedule Validator");
Expand All @@ -80,18 +84,42 @@ public void setGtfsSource(String source) {
gtfsInputField.setText(source);
}

public String getGtfsSource() {
return gtfsInputField.getText();
}

public void setOutputDirectory(Path outputDirectory) {
outputDirectoryField.setText(outputDirectory.toString());
}

public String getOutputDirectory() {
return outputDirectoryField.getText();
}

public void setNumThreads(int numThreads) {
numThreadsSpinner.setValue(numThreads);
}

public int getNumThreads() {
Object value = numThreadsSpinner.getValue();
if (value instanceof Integer) {
return (Integer) value;
}
return 0;
}

public void setCountryCode(String countryCode) {
countryCodeField.setText(countryCode);
}

public String getCountryCode() {
return countryCodeField.getText();
}

void addPreValidationCallback(Runnable callback) {
preValidationCallbacks.add(callback);
}

void constructUI() {
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

Expand Down Expand Up @@ -255,6 +283,10 @@ private boolean isValidationReadyToRun() {
}

private void runValidation() {
for (Runnable r : preValidationCallbacks) {
r.run();
}

try {
ValidationRunnerConfig config = buildConfig();
validationRunner.run(config, this);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package org.mobilitydata.gtfsvalidator.app.gui;

import java.nio.file.Path;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.prefs.Preferences;

/**
* Supports loading and saving application preferences from a persistent {@link Preferences} backend
* across application runs.
*/
public class GtfsValidatorPreferences {

private static final String KEY_GTFS_SOURCE = "gtfs_source";
private static final String KEY_OUTPUT_DIRECTORY = "output_directory";
private static final String KEY_NUM_THREADS = "num_threads";
private static final String KEY_COUNTRY_CODE = "country_code";

private final Preferences prefs;

public GtfsValidatorPreferences() {
this.prefs = Preferences.userNodeForPackage(GtfsValidatorPreferences.class);
}

public void loadPreferences(GtfsValidatorApp app) {
loadStringSetting(KEY_GTFS_SOURCE, app::setGtfsSource);
loadPathSetting(KEY_OUTPUT_DIRECTORY, app::setOutputDirectory);
loadIntSetting(KEY_NUM_THREADS, app::setNumThreads);
loadStringSetting(KEY_COUNTRY_CODE, app::setCountryCode);
}

public void savePreferences(GtfsValidatorApp app) {
saveStringSetting(app::getGtfsSource, KEY_GTFS_SOURCE);
saveStringSetting(app::getOutputDirectory, KEY_OUTPUT_DIRECTORY);
saveIntSetting(app::getNumThreads, KEY_NUM_THREADS);
saveStringSetting(app::getCountryCode, KEY_COUNTRY_CODE);
}

private void loadStringSetting(String key, Consumer<String> setter) {
String value = prefs.get(key, "");
if (!value.isBlank()) {
setter.accept(value);
}
}

private void loadIntSetting(String key, Consumer<Integer> setter) {
loadStringSetting(key, (value) -> setter.accept(Integer.parseInt(value)));
}

private void loadPathSetting(String key, Consumer<Path> setter) {
loadStringSetting(key, (value) -> setter.accept(Path.of(value)));
}

private void saveStringSetting(Supplier<String> getter, String key) {
String value = getter.get();
if (!value.isBlank()) {
prefs.put(key, value);
}
}

private void saveIntSetting(Supplier<Integer> getter, String key) {
saveStringSetting(
() -> {
Integer value = getter.get();
if (value == null || value == 0) {
return "";
}
return value.toString();
},
key);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,16 @@ public class Main {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();

public static void main(String[] args) {
Thread.setDefaultUncaughtExceptionHandler(new LogUncaughtExceptionHandler());

logger.atInfo().log("gtfs-validator: start");
SwingUtilities.invokeLater(() -> createAndShowGUI(args));
logger.atInfo().log("gtfs-validator: exit");
}

private static void createAndShowGUI(String[] args) {
Thread.currentThread().setUncaughtExceptionHandler(new LogUncaughtExceptionHandler());

try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Exception e) {
Expand All @@ -76,14 +80,26 @@ private static void createAndShowGUI(String[] args) {
GtfsValidatorApp app = new GtfsValidatorApp(runner, display);
app.constructUI();

GtfsValidatorPreferences prefs = new GtfsValidatorPreferences();
prefs.loadPreferences(app);
// We save preferences each time validation is run.
app.addPreValidationCallback(
() -> {
prefs.savePreferences(app);
});

// On Windows, if you drag a file onto the application shortcut, it will
// execute the app with the file as the first command-line argument. This
// doesn't appear to work on Mac OS.
if (args.length == 1) {
app.setGtfsSource(args[0]);
}

app.setOutputDirectory(getDefaultOutputDirectory());
// We set a default output directory as a fallback if the user didn't
// have one previously set.
if (app.getOutputDirectory().isBlank()) {
app.setOutputDirectory(getDefaultOutputDirectory());
}

app.pack();
// This causes the application window to center in the screen.
Expand Down Expand Up @@ -116,4 +132,14 @@ private static Path getDefaultOutputDirectory() {
}
return workingDirectory;
}

/**
* We introduce a catch-all for uncaught exceptions to make sure they make it into our application
* logs.
*/
public static class LogUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
public void uncaughtException(Thread thread, Throwable thrown) {
logger.atSevere().withCause(thrown).log("Uncaught application exception");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,17 @@ public void testValidationConfigWithAdvancedOptions() throws URISyntaxException
assertThat(config.numThreads()).isEqualTo(5);
assertThat(config.countryCode().getCountryCode()).isEqualTo("US");
}

@Test
public void testPreValidationCallback() throws URISyntaxException {
app.setGtfsSource("http://transit/gtfs.zip");
app.setOutputDirectory(Path.of("/path/to/output"));

Runnable callback = Mockito.mock(Runnable.class);
app.addPreValidationCallback(callback);

app.getValidateButtonForTesting().doClick();

verify(callback).run();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package org.mobilitydata.gtfsvalidator.app.gui;

import static com.google.common.truth.Truth.assertThat;

import java.nio.file.Path;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.mockito.quality.Strictness;

@RunWith(JUnit4.class)
public class GtfsValidatorPreferencesTest {

@Rule public MockitoRule rule = MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS);

@Mock private MonitoredValidationRunner runner;
@Mock private ValidationDisplay display;

@Test
public void testEndToEnd() {
{
GtfsValidatorApp source = new GtfsValidatorApp(runner, display);
source.setGtfsSource("http://gtfs.org/gtfs.zip");
source.setOutputDirectory(Path.of("/tmp/gtfs"));
source.setNumThreads(3);
source.setCountryCode("CA");

GtfsValidatorPreferences prefs = new GtfsValidatorPreferences();
prefs.savePreferences(source);
}

{
GtfsValidatorPreferences prefs = new GtfsValidatorPreferences();
GtfsValidatorApp dest = new GtfsValidatorApp(runner, display);
prefs.loadPreferences(dest);

assertThat(dest.getGtfsSource()).isEqualTo("http://gtfs.org/gtfs.zip");
assertThat(dest.getOutputDirectory()).isEqualTo("/tmp/gtfs");
assertThat(dest.getNumThreads()).isEqualTo(3);
assertThat(dest.getCountryCode()).isEqualTo("CA");
}
}
}
1 change: 1 addition & 0 deletions app/pkg/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ extraJavaModuleInfo {
requires('java.desktop')
requires('java.logging')
requires('java.naming')
requires('java.prefs')
requires('java.security.jgss')
requires('java.sql')
}
Expand Down

0 comments on commit 75c64a9

Please sign in to comment.