Skip to content

Commit 33ca9f3

Browse files
authored
station status or vehicle status should be required (#198)
1 parent 781f201 commit 33ca9f3

11 files changed

Lines changed: 280 additions & 56 deletions

File tree

example/pom.xml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
<maven.compiler.target>17</maven.compiler.target>
2121
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
2222
<!-- To use a snapshot, change this to e.g. 3.0.4-SNAPSHOT -->
23-
<gbfs-validator.version>3.0.3</gbfs-validator.version>
23+
<gbfs-validator.version>3.1.0</gbfs-validator.version>
2424
</properties>
2525

2626
<repositories>
@@ -44,6 +44,12 @@
4444
<artifactId>gbfs-validator-java</artifactId>
4545
<version>${gbfs-validator.version}</version>
4646
</dependency>
47+
<!-- Loader: fetches gbfs.json and all linked feed files automatically -->
48+
<dependency>
49+
<groupId>org.mobilitydata</groupId>
50+
<artifactId>gbfs-validator-java-loader</artifactId>
51+
<version>${gbfs-validator.version}</version>
52+
</dependency>
4753
</dependencies>
4854

4955

example/src/main/java/org/mobilitydata/gbfs/validator/example/GbfsValidatorExample.java

Lines changed: 53 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,25 @@
99
import java.net.http.HttpResponse;
1010
import java.nio.charset.StandardCharsets;
1111
import java.util.HashMap;
12+
import java.util.List;
1213
import java.util.Map;
1314
import org.mobilitydata.gbfs.validation.GbfsValidator;
1415
import org.mobilitydata.gbfs.validation.GbfsValidatorFactory;
1516
import org.mobilitydata.gbfs.validation.model.FileValidationError;
1617
import org.mobilitydata.gbfs.validation.model.FileValidationResult;
1718
import org.mobilitydata.gbfs.validation.model.ValidationResult;
19+
import org.mobilitydata.gbfs.validator.loader.LoadedFile;
20+
import org.mobilitydata.gbfs.validator.loader.Loader;
1821

1922
/**
2023
* Example showing how to validate a GBFS feed using gbfs-validator-java.
2124
*
22-
* <p>This example:
25+
* <p>Two use cases are demonstrated:
2326
* <ol>
24-
* <li>Fetches gbfs.json from a public GBFS feed</li>
25-
* <li>Validates the file using GbfsValidatorFactory</li>
26-
* <li>Prints the validation results to stdout</li>
27+
* <li>Single-file validation via {@link GbfsValidator#validateFile}</li>
28+
* <li>Full-feed validation via {@link Loader} + {@link GbfsValidator#validate}:
29+
* the Loader fetches gbfs.json and automatically discovers and loads all
30+
* linked feed files, so no manual URL construction is needed.</li>
2731
* </ol>
2832
*
2933
* <p>Usage:
@@ -42,57 +46,54 @@ public class GbfsValidatorExample {
4246
public static void main(String[] args) throws IOException, InterruptedException {
4347
System.out.println("=== GBFS Validator Java Example ===\n");
4448

49+
GbfsValidator validator = GbfsValidatorFactory.getGbfsJsonValidator();
50+
4551
// --- Example 1: Validate a single file ---
52+
// Useful when you already have the file content and just want schema validation.
4653
System.out.println("Example 1: Validate a single file");
4754
System.out.println("Fetching: " + GBFS_FEED_URL);
48-
String fileContents = fetchUrl(GBFS_FEED_URL);
49-
50-
GbfsValidator validator = GbfsValidatorFactory.getGbfsJsonValidator();
51-
InputStream fileStream = new ByteArrayInputStream(
52-
fileContents.getBytes(StandardCharsets.UTF_8)
53-
);
54-
55-
// The API expects file names WITHOUT the .json extension (e.g. "gbfs", not "gbfs.json")
56-
FileValidationResult fileResult = validator.validateFile("gbfs", fileStream);
55+
String gbfsContent = fetchUrl(GBFS_FEED_URL);
56+
InputStream gbfsStream = new ByteArrayInputStream(gbfsContent.getBytes(StandardCharsets.UTF_8));
57+
FileValidationResult fileResult = validator.validateFile("gbfs", gbfsStream);
5758
printFileResult(fileResult);
5859

59-
// --- Example 2: Validate a full feed (multiple files) ---
60+
// --- Example 2: Validate a full feed ---
61+
// The Loader fetches gbfs.json, parses the feed URLs from its discovery data,
62+
// and loads all linked files — handling language prefixes and auth automatically.
6063
System.out.println("\nExample 2: Validate a full feed");
61-
Map<String, InputStream> feedFiles = new HashMap<>();
62-
63-
// Keys must be the GBFS file type name (no .json extension)
64-
feedFiles.put("gbfs", new ByteArrayInputStream(
65-
fileContents.getBytes(StandardCharsets.UTF_8)
66-
));
67-
68-
// Fetch additional files — URL uses .json, but map key does not
69-
String[] additionalFileNames = {
70-
"system_information",
71-
"station_information",
72-
"station_status",
73-
"free_bike_status",
74-
};
75-
String baseUrl = GBFS_FEED_URL.substring(0, GBFS_FEED_URL.lastIndexOf('/') + 1);
76-
for (String fileType : additionalFileNames) {
77-
try {
78-
String content = fetchUrl(baseUrl + fileType + ".json");
79-
feedFiles.put(fileType, new ByteArrayInputStream(
80-
content.getBytes(StandardCharsets.UTF_8)
81-
));
82-
System.out.println(" Loaded: " + fileType);
83-
} catch (Exception e) {
84-
System.out.println(" Skipped: " + fileType + " (" + e.getMessage() + ")");
64+
Loader loader = new Loader();
65+
try {
66+
List<LoadedFile> loadedFiles = loader.load(GBFS_FEED_URL);
67+
68+
Map<String, InputStream> fileMap = new HashMap<>();
69+
for (LoadedFile file : loadedFiles) {
70+
// Keep the discovery file (no language) and only "en" language files.
71+
// If a feed does not publish "en", swap "en" for the desired language code.
72+
String lang = file.language();
73+
if (lang != null && !lang.equals("en")) {
74+
continue;
75+
}
76+
if (file.fileContents() != null) {
77+
System.out.println(" Loaded: " + file.fileName() + " (" + file.url() + ")");
78+
fileMap.put(file.fileName(), file.fileContents());
79+
} else {
80+
file.loaderErrors().forEach(e ->
81+
System.out.println(" Skipped: " + file.fileName()
82+
+ " (" + e.error() + ": " + e.message() + ")")
83+
);
84+
}
8585
}
86-
}
8786

88-
ValidationResult feedResult = validator.validate(feedFiles);
89-
printFeedResult(feedResult);
87+
ValidationResult feedResult = validator.validate(fileMap);
88+
printFeedResult(feedResult);
89+
} finally {
90+
loader.close();
91+
}
9092
}
9193

9294
private static void printFileResult(FileValidationResult result) {
9395
System.out.println(" File : " + result.file());
9496
System.out.println(" Version : " + result.version());
95-
System.out.println(" Schema : " + result.schema());
9697
System.out.println(" Exists : " + result.exists());
9798
System.out.println(" Required: " + result.required());
9899
System.out.println(" Errors : " + result.errorsCount());
@@ -115,14 +116,17 @@ private static void printFileResult(FileValidationResult result) {
115116

116117
private static void printFeedResult(ValidationResult result) {
117118
System.out.println(" Summary : " + result.summary());
118-
System.out.println(" Files validated: " + result.files().size());
119-
result.files().forEach((name, fileResult) -> {
119+
var presentFiles = result.files().entrySet().stream()
120+
.filter(e -> e.getValue().exists())
121+
.toList();
122+
System.out.println(" Files validated: " + presentFiles.size());
123+
presentFiles.forEach(e -> {
120124
System.out.printf(" %-35s errors=%d version=%s%n",
121-
name, fileResult.errorsCount(), fileResult.version());
125+
e.getKey(), e.getValue().errorsCount(), e.getValue().version());
122126
});
123127

124-
long totalErrors = result.files().values().stream()
125-
.mapToLong(FileValidationResult::errorsCount)
128+
long totalErrors = presentFiles.stream()
129+
.mapToLong(e -> e.getValue().errorsCount())
126130
.sum();
127131
System.out.println("\n Total errors across all files: " + totalErrors);
128132

@@ -139,12 +143,11 @@ private static String fetchUrl(String url) throws IOException, InterruptedExcept
139143
.uri(URI.create(url))
140144
.GET()
141145
.build();
142-
HttpResponse<String> response = client.send(
143-
request, HttpResponse.BodyHandlers.ofString()
144-
);
146+
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
145147
if (response.statusCode() != 200) {
146148
throw new IOException("HTTP " + response.statusCode() + " for " + url);
147149
}
148150
return response.body();
149151
}
150152
}
153+

gbfs-validator-java-cli/src/main/java/org/mobilitydata/gbfs/validator/cli/formatter/ConsoleReportFormatter.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ public String format(
8282

8383
if (fileResult.required()) {
8484
sb.append(" [REQUIRED]");
85+
} else if (fileResult.conditionallyRequired()) {
86+
sb.append(" [CONDITIONALLY REQUIRED]");
8587
}
8688

8789
if (!fileResult.exists()) {

gbfs-validator-java/src/main/java/org/mobilitydata/gbfs/validation/model/FileValidationResult.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
* The result of validating a single GBFS file
2828
* @param file The name of the file that was validated
2929
* @param required Whether the file is required in the given version of GBFS
30+
* @param conditionallyRequired Whether the file is conditionally required (e.g. station_status or
31+
* vehicle_status must be present — neither is individually required, but at least one must exist)
3032
* @param exists Whether the file existed in the validation input
3133
* @param errorsCount The number of errors found while validating the file
3234
* @param schema The schema used to validate the file
@@ -38,6 +40,7 @@
3840
public record FileValidationResult(
3941
String file,
4042
boolean required,
43+
boolean conditionallyRequired,
4144
boolean exists,
4245
int errorsCount,
4346
String schema,
@@ -61,6 +64,8 @@ public String toString() {
6164
'\'' +
6265
", required=" +
6366
required +
67+
", conditionallyRequired=" +
68+
conditionallyRequired +
6469
", exists=" +
6570
exists +
6671
", errorsCount=" +
@@ -86,6 +91,7 @@ public String toString() {
8691
public boolean sameAs(FileValidationResult other) {
8792
if (other == null) return false;
8893
if (required != other.required) return false;
94+
if (conditionallyRequired != other.conditionallyRequired) return false;
8995
if (exists != other.exists) return false;
9096
if (errorsCount != other.errorsCount) return false; // This should ideally reflect both validation and system errors count
9197
if (!Objects.equals(file, other.file)) return false;

gbfs-validator-java/src/main/java/org/mobilitydata/gbfs/validation/validator/FileValidator.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,13 +80,14 @@ public FileValidationResult validate(
8080
return new FileValidationResult(
8181
feedName,
8282
isRequired(feedName),
83+
false,
8384
feed != null,
8485
errorsCount,
8586
schema.toString(),
8687
Optional.ofNullable(feed).map(JSONObject::toString).orElse(null),
8788
version.getVersionString(),
8889
validationErrors,
89-
java.util.Collections.emptyList() // Added for systemErrors
90+
java.util.Collections.emptyList()
9091
);
9192
}
9293

@@ -130,6 +131,7 @@ public FileValidationResult validateMissingFile(String file) {
130131
file,
131132
isRequired,
132133
false,
134+
false,
133135
isRequired ? 1 : 0,
134136
version.getSchema(file).toString(),
135137
null,

gbfs-validator-java/src/main/java/org/mobilitydata/gbfs/validation/validator/GbfsJsonValidator.java

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ public ValidationResult validate(Map<String, InputStream> rawFeeds) {
131131

132132
List<String> missingFiles = findMissingFiles(version, fileValidations);
133133
handleMissingFiles(fileValidations, missingFiles, version); // This creates FVRs for missing files
134+
checkStatusFilePresence(fileValidations, version);
134135

135136
ValidationSummary summary = new ValidationSummary(
136137
version.getVersionString(),
@@ -147,6 +148,60 @@ public ValidationResult validate(Map<String, InputStream> rawFeeds) {
147148
return new ValidationResult(summary, fileValidations);
148149
}
149150

151+
/**
152+
* Per GBFS spec, a feed must be dock-based, free-floating, or hybrid, so at least one of
153+
* station_status / vehicle_status (v3+) or station_status / free_bike_status (pre-v3) must
154+
* be present. If neither is present, both are flagged as required with an error.
155+
*/
156+
private void checkStatusFilePresence(
157+
Map<String, FileValidationResult> fileValidations,
158+
Version version
159+
) {
160+
String freeFloatingFile = version.getFileNames().contains("vehicle_status")
161+
? "vehicle_status"
162+
: "free_bike_status";
163+
164+
boolean stationStatusAbsent = !isPresent(fileValidations, "station_status");
165+
boolean freeFloatingAbsent = !isPresent(fileValidations, freeFloatingFile);
166+
167+
if (stationStatusAbsent && freeFloatingAbsent) {
168+
markAsConditionallyRequired(fileValidations, "station_status");
169+
markAsConditionallyRequired(fileValidations, freeFloatingFile);
170+
}
171+
}
172+
173+
private boolean isPresent(
174+
Map<String, FileValidationResult> fileValidations,
175+
String file
176+
) {
177+
FileValidationResult result = fileValidations.get(file);
178+
return result != null && result.exists();
179+
}
180+
181+
private void markAsConditionallyRequired(
182+
Map<String, FileValidationResult> fileValidations,
183+
String file
184+
) {
185+
FileValidationResult existing = fileValidations.get(file);
186+
if (existing != null) {
187+
fileValidations.put(
188+
file,
189+
new FileValidationResult(
190+
existing.file(),
191+
false,
192+
true,
193+
false,
194+
1,
195+
existing.schema(),
196+
null,
197+
existing.version(),
198+
Collections.emptyList(),
199+
Collections.emptyList()
200+
)
201+
);
202+
}
203+
}
204+
150205
private Version detectVersionFromParsedFeeds(
151206
Map<String, ParsedFeedContainer> parsedFeeds
152207
) {
@@ -357,6 +412,7 @@ private FileValidationResult createParsingErrorResult(
357412
return new FileValidationResult(
358413
feedName,
359414
supportedFeed && schemaVersion.isFileRequired(feedName),
415+
false,
360416
true,
361417
0,
362418
supportedFeed ? schemaVersion.getSchema(feedName).toString() : null,

gbfs-validator-java/src/test/java/org/mobilitydata/gbfs/validation/model/ValidationResultTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ private FileValidationResult generateFileValidationResult(
6464
return new FileValidationResult(
6565
"gbfs",
6666
true,
67+
false,
6768
true,
6869
2,
6970
null,

0 commit comments

Comments
 (0)