Skip to content

Commit cec914e

Browse files
authored
feat: Check for new version of app during validation (#1176)
* Add logic to resolve the current app version and notify if a new version is available. * Bump test timeout. * Try again on that unit test. * Various fixes in response to PR comments. * Update to point to Releases page for new downloads. * Format code.
1 parent 187be96 commit cec914e

File tree

16 files changed

+504
-16
lines changed

16 files changed

+504
-16
lines changed

app/gui/src/main/java/org/mobilitydata/gtfsvalidator/app/gui/GtfsValidatorApp.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,15 @@
1616
package org.mobilitydata.gtfsvalidator.app.gui;
1717

1818
import com.google.common.flogger.FluentLogger;
19+
import java.awt.Color;
1920
import java.awt.Component;
21+
import java.awt.Cursor;
2022
import java.awt.Dimension;
2123
import java.awt.Font;
2224
import java.awt.GridBagConstraints;
2325
import java.awt.GridBagLayout;
26+
import java.awt.event.MouseAdapter;
27+
import java.awt.event.MouseEvent;
2428
import java.io.File;
2529
import java.net.URI;
2630
import java.net.URISyntaxException;
@@ -53,12 +57,15 @@ public class GtfsValidatorApp extends JFrame {
5357
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
5458

5559
private static final Dimension VERTICAL_GAP = new Dimension(0, 40);
60+
private static final Dimension TEXT_GAP = new Dimension(0, 10);
5661

5762
private static final Font BOLD_FONT = createBoldFont();
5863

5964
private final JTextField gtfsInputField = new JTextField();
6065
private final JTextField outputDirectoryField = new JTextField();
6166

67+
private final JPanel newVersionAvailablePanel = new JPanel();
68+
6269
private final JButton validateButton = new JButton();
6370

6471
private final JPanel advancedOptionsPanel = new JPanel();
@@ -120,6 +127,11 @@ void addPreValidationCallback(Runnable callback) {
120127
preValidationCallbacks.add(callback);
121128
}
122129

130+
public void showNewVersionAvailable() {
131+
newVersionAvailablePanel.setVisible(true);
132+
pack();
133+
}
134+
123135
void constructUI() {
124136
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
125137

@@ -133,6 +145,7 @@ void constructUI() {
133145
constructOutputDirectorySection(panel);
134146
panel.add(Box.createRigidArea(VERTICAL_GAP));
135147
constructAdvancedOptionsPanel(panel);
148+
constructNewVersionAvailablePanel(panel);
136149
constructValidateButton(panel);
137150

138151
// Ensure everything is left-aligned in the main application panel.
@@ -247,6 +260,32 @@ private void constructAdvancedOptionsPanel(JPanel parent) {
247260
advancedOptionsPanel.setVisible(false);
248261
}
249262

263+
private void constructNewVersionAvailablePanel(JPanel parent) {
264+
// Panel is initially not shown.
265+
newVersionAvailablePanel.setVisible(false);
266+
newVersionAvailablePanel.setLayout(
267+
new BoxLayout(newVersionAvailablePanel, BoxLayout.PAGE_AXIS));
268+
parent.add(newVersionAvailablePanel);
269+
270+
newVersionAvailablePanel.add(
271+
createLabelWithFont(bundle.getString("new_version_available"), BOLD_FONT));
272+
newVersionAvailablePanel.add(Box.createRigidArea(TEXT_GAP));
273+
274+
JLabel download_link = new JLabel(bundle.getString("download_here"));
275+
download_link.setForeground(Color.BLUE.darker());
276+
download_link.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
277+
download_link.addMouseListener(
278+
new MouseAdapter() {
279+
@Override
280+
public void mouseClicked(MouseEvent e) {
281+
validationDisplay.handleBrowseToHomepage();
282+
}
283+
});
284+
newVersionAvailablePanel.add(download_link);
285+
286+
newVersionAvailablePanel.add(Box.createRigidArea(VERTICAL_GAP));
287+
}
288+
250289
private void constructValidateButton(JPanel panel) {
251290
JPanel validateButtonPanel = new JPanel();
252291
validateButtonPanel.setLayout(new BoxLayout(validateButtonPanel, BoxLayout.LINE_AXIS));

app/gui/src/main/java/org/mobilitydata/gtfsvalidator/app/gui/Main.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import javax.swing.SwingUtilities;
2727
import javax.swing.UIManager;
2828
import org.mobilitydata.gtfsvalidator.runner.ValidationRunner;
29+
import org.mobilitydata.gtfsvalidator.util.VersionResolver;
2930

3031
/**
3132
* The main entry point for the GUI application.
@@ -74,9 +75,10 @@ private static void createAndShowGUI(String[] args) {
7475
logger.atSevere().withCause(e).log("Error setting system look-and-feel");
7576
}
7677

78+
VersionResolver resolver = new VersionResolver();
7779
ValidationDisplay display = new ValidationDisplay();
7880
MonitoredValidationRunner runner =
79-
new MonitoredValidationRunner(new ValidationRunner(), display);
81+
new MonitoredValidationRunner(new ValidationRunner(resolver), display);
8082
GtfsValidatorApp app = new GtfsValidatorApp(runner, display);
8183
app.constructUI();
8284

@@ -88,6 +90,14 @@ private static void createAndShowGUI(String[] args) {
8890
prefs.savePreferences(app);
8991
});
9092

93+
// Check to see if there is a new version of the app available.
94+
resolver.addCallback(
95+
(versionInfo) -> {
96+
if (versionInfo.updateAvailable()) {
97+
SwingUtilities.invokeLater(() -> app.showNewVersionAvailable());
98+
}
99+
});
100+
91101
// On Windows, if you drag a file onto the application shortcut, it will
92102
// execute the app with the file as the first command-line argument. This
93103
// doesn't appear to work on Mac OS.

app/gui/src/main/java/org/mobilitydata/gtfsvalidator/app/gui/ValidationDisplay.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.google.common.flogger.FluentLogger;
44
import java.awt.Desktop;
55
import java.io.IOException;
6+
import java.net.URI;
67
import java.nio.file.Path;
78
import javax.swing.JOptionPane;
89
import org.mobilitydata.gtfsvalidator.runner.ValidationRunner;
@@ -43,4 +44,13 @@ void handleError() {
4344
JOptionPane.ERROR_MESSAGE);
4445
System.exit(-1);
4546
}
47+
48+
void handleBrowseToHomepage() {
49+
try {
50+
Desktop.getDesktop()
51+
.browse(URI.create("https://github.com/MobilityData/gtfs-validator/releases"));
52+
} catch (IOException ex) {
53+
logger.atSevere().withCause(ex).log("Error opening webpage");
54+
}
55+
}
4656
}

app/gui/src/main/resources/org/mobilitydata/gtfsvalidator/app/gui/GtfsValidatorApp.properties

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ output_directory=Output Directory:
88
choose_output_directory=Choose Output Directory...
99
output_directory_description=The validation report will be written here.
1010

11+
new_version_available=A new version of the Canonical GTFS Schedule validator is available!
12+
download_here=Download here to get the latest/best validation results.
13+
1114
advanced=Advanced
1215
advanced_options=Advanced Options
1316
number_of_threads=Number of threads used to run the validator:

core/build.gradle

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,13 @@ dependencies {
4848
testImplementation 'com.google.truth.extensions:truth-java8-extension:1.0.1'
4949
}
5050

51+
jar {
52+
manifest {
53+
attributes('Implementation-Title': 'gtfs-validator-core',
54+
'Implementation-Version': project.version)
55+
}
56+
}
57+
5158
publishing {
5259
publications {
5360
mavenJava(MavenPublication) {

main/build.gradle

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,11 @@ dependencies {
7777
implementation 'com.univocity:univocity-parsers:2.9.0'
7878
implementation 'com.google.geometry:s2-geometry:2.0.0'
7979
implementation 'org.thymeleaf:thymeleaf:3.0.15.RELEASE'
80-
implementation 'com.jcabi:jcabi-manifests:1.1'
80+
implementation 'io.github.classgraph:classgraph:4.8.146'
8181
testImplementation group: 'junit', name: 'junit', version: '4.13'
8282
testImplementation 'com.google.truth:truth:1.0.1'
8383
testImplementation 'com.google.truth.extensions:truth-java8-extension:1.0.1'
84+
testImplementation 'org.mockito:mockito-core:4.5.1'
8485
}
8586

8687
test {
@@ -91,6 +92,8 @@ test {
9192
testLogging {
9293
events "passed", "skipped", "failed"
9394
}
95+
96+
systemProperty 'gtfsValidatorVersionForTest', project.version
9497
}
9598

9699
// Share the test report data to be aggregated for the whole project

main/src/main/java/org/mobilitydata/gtfsvalidator/cli/Main.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import java.nio.file.Paths;
2828
import org.mobilitydata.gtfsvalidator.notice.NoticeSchemaGenerator;
2929
import org.mobilitydata.gtfsvalidator.runner.ValidationRunner;
30+
import org.mobilitydata.gtfsvalidator.util.VersionResolver;
3031

3132
/** The main entry point for GTFS Validator CLI. */
3233
public class Main {
@@ -41,14 +42,16 @@ public static void main(String[] argv) {
4142
}
4243

4344
try {
44-
ValidationRunner runner = new ValidationRunner();
45+
ValidationRunner runner = new ValidationRunner(new VersionResolver());
4546
if (runner.run(args.toConfig()) != ValidationRunner.Status.SUCCESS) {
4647
System.exit(-1);
4748
}
4849
} catch (Exception ex) {
4950
logger.atSevere().withCause(ex).log("Error running validation");
5051
System.exit(-1);
5152
}
53+
54+
System.exit(0);
5255
}
5356

5457
/**

main/src/main/java/org/mobilitydata/gtfsvalidator/report/HtmlReportGenerator.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
package org.mobilitydata.gtfsvalidator.report;
1818

19-
import com.jcabi.manifests.Manifests;
2019
import java.io.FileWriter;
2120
import java.io.IOException;
2221
import java.nio.file.Path;
@@ -25,6 +24,7 @@
2524
import org.mobilitydata.gtfsvalidator.notice.NoticeContainer;
2625
import org.mobilitydata.gtfsvalidator.report.model.ReportSummary;
2726
import org.mobilitydata.gtfsvalidator.runner.ValidationRunnerConfig;
27+
import org.mobilitydata.gtfsvalidator.util.VersionInfo;
2828
import org.thymeleaf.TemplateEngine;
2929
import org.thymeleaf.context.Context;
3030
import org.thymeleaf.templatemode.TemplateMode;
@@ -35,15 +35,17 @@ public class HtmlReportGenerator {
3535

3636
/** Generate the HTML report using the class ReportSummary and the notice container. */
3737
public void generateReport(
38-
NoticeContainer noticeContainer, ValidationRunnerConfig config, Path reportPath)
38+
NoticeContainer noticeContainer,
39+
ValidationRunnerConfig config,
40+
VersionInfo versionInfo,
41+
Path reportPath)
3942
throws IOException {
4043
TemplateEngine templateEngine = new TemplateEngine();
4144
ClassLoaderTemplateResolver templateResolver = new ClassLoaderTemplateResolver();
4245
templateResolver.setTemplateMode(TemplateMode.HTML);
4346
templateEngine.setTemplateResolver(templateResolver);
4447

45-
ReportSummary summary = new ReportSummary(noticeContainer);
46-
String version = Manifests.read("Implementation-Version");
48+
ReportSummary summary = new ReportSummary(noticeContainer, versionInfo);
4749

4850
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd 'at' HH:mm:ss z");
4951
Date now = new Date(System.currentTimeMillis());
@@ -52,7 +54,6 @@ public void generateReport(
5254
Context context = new Context();
5355
context.setVariable("summary", summary);
5456
context.setVariable("config", config);
55-
context.setVariable("version", version);
5657
context.setVariable("date", date);
5758

5859
try (FileWriter writer = new FileWriter(reportPath.toFile())) {

main/src/main/java/org/mobilitydata/gtfsvalidator/report/model/ReportSummary.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,16 @@
2424
import org.mobilitydata.gtfsvalidator.notice.Notice;
2525
import org.mobilitydata.gtfsvalidator.notice.NoticeContainer;
2626
import org.mobilitydata.gtfsvalidator.notice.SeverityLevel;
27+
import org.mobilitydata.gtfsvalidator.util.VersionInfo;
2728

2829
/** ReportSummary is the class containing the summary methods for the HTML report. */
2930
public class ReportSummary {
3031
private final NoticeContainer container;
3132
private final Map<SeverityLevel, Long> severityCounts;
3233
private final Map<SeverityLevel, Map<String, List<NoticeView>>> noticesMap;
34+
private final VersionInfo versionInfo;
3335

34-
public ReportSummary(NoticeContainer container) {
36+
public ReportSummary(NoticeContainer container, VersionInfo versionInfo) {
3537
this.container = container;
3638
this.severityCounts =
3739
container.getValidationNotices().stream()
@@ -44,6 +46,7 @@ public ReportSummary(NoticeContainer container) {
4446
NoticeView::getSeverityLevel,
4547
LinkedHashMap::new,
4648
Collectors.groupingBy(NoticeView::getCode, TreeMap::new, Collectors.toList())));
49+
this.versionInfo = versionInfo;
4750
}
4851

4952
/**
@@ -93,4 +96,12 @@ public long getWarningCount() {
9396
public long getInfoCount() {
9497
return severityCounts.getOrDefault(SeverityLevel.INFO, 0L);
9598
}
99+
100+
public String getVersion() {
101+
return versionInfo.currentVersion().orElse(null);
102+
}
103+
104+
public boolean isNewVersionOfValidatorAvailable() {
105+
return versionInfo.updateAvailable();
106+
}
96107
}

main/src/main/java/org/mobilitydata/gtfsvalidator/runner/ValidationRunner.java

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import java.nio.charset.StandardCharsets;
2626
import java.nio.file.Files;
2727
import java.nio.file.Paths;
28+
import java.time.Duration;
2829
import java.time.ZoneId;
2930
import java.time.ZonedDateTime;
3031
import org.mobilitydata.gtfsvalidator.input.CurrentDateTime;
@@ -35,6 +36,8 @@
3536
import org.mobilitydata.gtfsvalidator.report.HtmlReportGenerator;
3637
import org.mobilitydata.gtfsvalidator.table.GtfsFeedContainer;
3738
import org.mobilitydata.gtfsvalidator.table.GtfsFeedLoader;
39+
import org.mobilitydata.gtfsvalidator.util.VersionInfo;
40+
import org.mobilitydata.gtfsvalidator.util.VersionResolver;
3841
import org.mobilitydata.gtfsvalidator.validator.DefaultValidatorProvider;
3942
import org.mobilitydata.gtfsvalidator.validator.ValidationContext;
4043
import org.mobilitydata.gtfsvalidator.validator.ValidatorLoader;
@@ -46,6 +49,8 @@ public class ValidationRunner {
4649
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
4750
private static final String GTFS_ZIP_FILENAME = "gtfs.zip";
4851

52+
private final VersionResolver versionResolver;
53+
4954
public enum Status {
5055
// Indicates validation successfully completed, but doesn't imply the
5156
// feed itself is valid.
@@ -58,7 +63,17 @@ public enum Status {
5863
EXCEPTION
5964
}
6065

66+
public ValidationRunner(VersionResolver versionResolver) {
67+
this.versionResolver = versionResolver;
68+
}
69+
6170
public Status run(ValidationRunnerConfig config) {
71+
VersionInfo versionInfo = versionResolver.getVersionInfoWithTimeout(Duration.ofSeconds(5));
72+
logger.atInfo().log("VersionInfo: %s", versionInfo);
73+
if (versionInfo.updateAvailable()) {
74+
logger.atInfo().log("A new version of the validator is available!");
75+
}
76+
6277
ValidatorLoader validatorLoader = null;
6378
try {
6479
validatorLoader = new ValidatorLoader();
@@ -87,7 +102,7 @@ public Status run(ValidationRunnerConfig config) {
87102
noticeContainer.addSystemError(new URISyntaxError(e));
88103
}
89104
if (gtfsInput == null) {
90-
exportReport(noticeContainer, config);
105+
exportReport(noticeContainer, config, versionInfo);
91106
if (!noticeContainer.getSystemErrors().isEmpty()) {
92107
return Status.SYSTEM_ERRORS;
93108
} else {
@@ -110,7 +125,7 @@ public Status run(ValidationRunnerConfig config) {
110125
closeGtfsInput(gtfsInput, noticeContainer);
111126

112127
// Output
113-
exportReport(noticeContainer, config);
128+
exportReport(noticeContainer, config, versionInfo);
114129
printSummary(startNanos, feedContainer);
115130
return Status.SUCCESS;
116131
}
@@ -191,7 +206,7 @@ private static Gson createGson(boolean pretty) {
191206

192207
/** Generates and exports reports for both validation notices and system errors reports. */
193208
public static void exportReport(
194-
final NoticeContainer noticeContainer, final ValidationRunnerConfig config) {
209+
NoticeContainer noticeContainer, ValidationRunnerConfig config, VersionInfo versionInfo) {
195210
if (!Files.exists(config.outputDirectory())) {
196211
try {
197212
Files.createDirectories(config.outputDirectory());
@@ -207,7 +222,10 @@ public static void exportReport(
207222
config.outputDirectory().resolve(config.validationReportFileName()),
208223
gson.toJson(noticeContainer.exportValidationNotices()).getBytes(StandardCharsets.UTF_8));
209224
generator.generateReport(
210-
noticeContainer, config, config.outputDirectory().resolve(config.htmlReportFileName()));
225+
noticeContainer,
226+
config,
227+
versionInfo,
228+
config.outputDirectory().resolve(config.htmlReportFileName()));
211229
Files.write(
212230
config.outputDirectory().resolve(config.systemErrorsReportFileName()),
213231
gson.toJson(noticeContainer.exportSystemErrors()).getBytes(StandardCharsets.UTF_8));

0 commit comments

Comments
 (0)