Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Persist GUI app settings between executions #1169

Merged
merged 6 commits into from
May 27, 2022
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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