Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
8 changes: 7 additions & 1 deletion example/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- To use a snapshot, change this to e.g. 3.0.4-SNAPSHOT -->
<gbfs-validator.version>3.0.3</gbfs-validator.version>
<gbfs-validator.version>3.1.0</gbfs-validator.version>
</properties>

<repositories>
Expand All @@ -44,6 +44,12 @@
<artifactId>gbfs-validator-java</artifactId>
<version>${gbfs-validator.version}</version>
</dependency>
<!-- Loader: fetches gbfs.json and all linked feed files automatically -->
<dependency>
<groupId>org.mobilitydata</groupId>
<artifactId>gbfs-validator-java-loader</artifactId>
<version>${gbfs-validator.version}</version>
</dependency>
</dependencies>


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,25 @@
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.mobilitydata.gbfs.validation.GbfsValidator;
import org.mobilitydata.gbfs.validation.GbfsValidatorFactory;
import org.mobilitydata.gbfs.validation.model.FileValidationError;
import org.mobilitydata.gbfs.validation.model.FileValidationResult;
import org.mobilitydata.gbfs.validation.model.ValidationResult;
import org.mobilitydata.gbfs.validator.loader.LoadedFile;
import org.mobilitydata.gbfs.validator.loader.Loader;

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

GbfsValidator validator = GbfsValidatorFactory.getGbfsJsonValidator();

// --- Example 1: Validate a single file ---
// Useful when you already have the file content and just want schema validation.
System.out.println("Example 1: Validate a single file");
System.out.println("Fetching: " + GBFS_FEED_URL);
String fileContents = fetchUrl(GBFS_FEED_URL);

GbfsValidator validator = GbfsValidatorFactory.getGbfsJsonValidator();
InputStream fileStream = new ByteArrayInputStream(
fileContents.getBytes(StandardCharsets.UTF_8)
);

// The API expects file names WITHOUT the .json extension (e.g. "gbfs", not "gbfs.json")
FileValidationResult fileResult = validator.validateFile("gbfs", fileStream);
String gbfsContent = fetchUrl(GBFS_FEED_URL);
InputStream gbfsStream = new ByteArrayInputStream(gbfsContent.getBytes(StandardCharsets.UTF_8));
FileValidationResult fileResult = validator.validateFile("gbfs", gbfsStream);
printFileResult(fileResult);

// --- Example 2: Validate a full feed (multiple files) ---
// --- Example 2: Validate a full feed ---
// The Loader fetches gbfs.json, parses the feed URLs from its discovery data,
// and loads all linked files — handling language prefixes and auth automatically.
System.out.println("\nExample 2: Validate a full feed");
Map<String, InputStream> feedFiles = new HashMap<>();

// Keys must be the GBFS file type name (no .json extension)
feedFiles.put("gbfs", new ByteArrayInputStream(
fileContents.getBytes(StandardCharsets.UTF_8)
));

// Fetch additional files — URL uses .json, but map key does not
String[] additionalFileNames = {
"system_information",
"station_information",
"station_status",
"free_bike_status",
};
String baseUrl = GBFS_FEED_URL.substring(0, GBFS_FEED_URL.lastIndexOf('/') + 1);
for (String fileType : additionalFileNames) {
try {
String content = fetchUrl(baseUrl + fileType + ".json");
feedFiles.put(fileType, new ByteArrayInputStream(
content.getBytes(StandardCharsets.UTF_8)
));
System.out.println(" Loaded: " + fileType);
} catch (Exception e) {
System.out.println(" Skipped: " + fileType + " (" + e.getMessage() + ")");
Loader loader = new Loader();
try {
List<LoadedFile> loadedFiles = loader.load(GBFS_FEED_URL);

Map<String, InputStream> fileMap = new HashMap<>();
for (LoadedFile file : loadedFiles) {
// Keep the discovery file (no language) and only "en" language files.
// If a feed does not publish "en", swap "en" for the desired language code.
String lang = file.language();
if (lang != null && !lang.equals("en")) {
continue;
}
if (file.fileContents() != null) {
System.out.println(" Loaded: " + file.fileName() + " (" + file.url() + ")");
fileMap.put(file.fileName(), file.fileContents());
} else {
file.loaderErrors().forEach(e ->
System.out.println(" Skipped: " + file.fileName()
+ " (" + e.error() + ": " + e.message() + ")")
);
}
}
}

ValidationResult feedResult = validator.validate(feedFiles);
printFeedResult(feedResult);
ValidationResult feedResult = validator.validate(fileMap);
printFeedResult(feedResult);
} finally {
loader.close();
}
}

private static void printFileResult(FileValidationResult result) {
System.out.println(" File : " + result.file());
System.out.println(" Version : " + result.version());
System.out.println(" Schema : " + result.schema());
System.out.println(" Exists : " + result.exists());
System.out.println(" Required: " + result.required());
System.out.println(" Errors : " + result.errorsCount());
Expand All @@ -115,14 +116,17 @@ private static void printFileResult(FileValidationResult result) {

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

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

Expand All @@ -139,12 +143,11 @@ private static String fetchUrl(String url) throws IOException, InterruptedExcept
.uri(URI.create(url))
.GET()
.build();
HttpResponse<String> response = client.send(
request, HttpResponse.BodyHandlers.ofString()
);
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new IOException("HTTP " + response.statusCode() + " for " + url);
}
return response.body();
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ public String format(

if (fileResult.required()) {
sb.append(" [REQUIRED]");
} else if (fileResult.conditionallyRequired()) {
sb.append(" [CONDITIONALLY REQUIRED]");
}

if (!fileResult.exists()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
* The result of validating a single GBFS file
* @param file The name of the file that was validated
* @param required Whether the file is required in the given version of GBFS
* @param conditionallyRequired Whether the file is conditionally required (e.g. station_status or
* vehicle_status must be present — neither is individually required, but at least one must exist)
* @param exists Whether the file existed in the validation input
* @param errorsCount The number of errors found while validating the file
* @param schema The schema used to validate the file
Expand All @@ -38,6 +40,7 @@
public record FileValidationResult(
String file,
boolean required,
boolean conditionallyRequired,
boolean exists,
int errorsCount,
String schema,
Expand All @@ -61,6 +64,8 @@ public String toString() {
'\'' +
", required=" +
required +
", conditionallyRequired=" +
conditionallyRequired +
", exists=" +
exists +
", errorsCount=" +
Expand All @@ -86,6 +91,7 @@ public String toString() {
public boolean sameAs(FileValidationResult other) {
if (other == null) return false;
if (required != other.required) return false;
if (conditionallyRequired != other.conditionallyRequired) return false;
if (exists != other.exists) return false;
if (errorsCount != other.errorsCount) return false; // This should ideally reflect both validation and system errors count
if (!Objects.equals(file, other.file)) return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,14 @@ public FileValidationResult validate(
return new FileValidationResult(
feedName,
isRequired(feedName),
false,
feed != null,
errorsCount,
schema.toString(),
Optional.ofNullable(feed).map(JSONObject::toString).orElse(null),
version.getVersionString(),
validationErrors,
java.util.Collections.emptyList() // Added for systemErrors
java.util.Collections.emptyList()
);
}

Expand Down Expand Up @@ -130,6 +131,7 @@ public FileValidationResult validateMissingFile(String file) {
file,
isRequired,
false,
false,
isRequired ? 1 : 0,
version.getSchema(file).toString(),
null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ public ValidationResult validate(Map<String, InputStream> rawFeeds) {

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

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

/**
* Per GBFS spec, a feed must be dock-based, free-floating, or hybrid, so at least one of
* station_status / vehicle_status (v3+) or station_status / free_bike_status (pre-v3) must
* be present. If neither is present, both are flagged as required with an error.
*/
private void checkStatusFilePresence(
Map<String, FileValidationResult> fileValidations,
Version version
) {
String freeFloatingFile = version.getFileNames().contains("vehicle_status")
? "vehicle_status"
: "free_bike_status";

boolean stationStatusAbsent = !isPresent(fileValidations, "station_status");
boolean freeFloatingAbsent = !isPresent(fileValidations, freeFloatingFile);

if (stationStatusAbsent && freeFloatingAbsent) {
markAsConditionallyRequired(fileValidations, "station_status");
markAsConditionallyRequired(fileValidations, freeFloatingFile);
}
}

private boolean isPresent(
Map<String, FileValidationResult> fileValidations,
String file
) {
FileValidationResult result = fileValidations.get(file);
return result != null && result.exists();
}

private void markAsConditionallyRequired(
Map<String, FileValidationResult> fileValidations,
String file
) {
FileValidationResult existing = fileValidations.get(file);
if (existing != null) {
fileValidations.put(
file,
new FileValidationResult(
existing.file(),
false,
true,
false,
1,
existing.schema(),
null,
existing.version(),
Collections.emptyList(),
Collections.emptyList()
)
);
}
}

private Version detectVersionFromParsedFeeds(
Map<String, ParsedFeedContainer> parsedFeeds
) {
Expand Down Expand Up @@ -357,6 +412,7 @@ private FileValidationResult createParsingErrorResult(
return new FileValidationResult(
feedName,
supportedFeed && schemaVersion.isFileRequired(feedName),
false,
true,
0,
supportedFeed ? schemaVersion.getSchema(feedName).toString() : null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ private FileValidationResult generateFileValidationResult(
return new FileValidationResult(
"gbfs",
true,
false,
true,
2,
null,
Expand Down
Loading
Loading