diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8ee69e5..58ca1fe 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,4 +25,4 @@ jobs: ${{ runner.os }}-maven- ${{ runner.os }}- - name: Run maven build - run: mvn install + run: mvn install -PprettierCheck -Dprettier.nodePath=node -Dprettier.npmPath=npm diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index e953a20..b2655fb 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -40,7 +40,7 @@ jobs: ${{ runner.os }}- - name: Run maven build - run: mvn verify -s .github/workflows/settings.xml + run: mvn verify -s .github/workflows/settings.xml -PprettierCheck -Dprettier.nodePath=node -Dprettier.npmPath=npm - name: Sonar Scan env: SONAR_TOKEN: ${{ secrets.ENTUR_SONAR_PASSWORD }} @@ -66,4 +66,4 @@ jobs: with: push_to_repo: true snapshot: false - next_version: '' \ No newline at end of file + next_version: '' diff --git a/gbfs-validator-java-api/src/main/java/org/entur/gbfs/validator/api/handler/LoaderConfiguration.java b/gbfs-validator-java-api/src/main/java/org/entur/gbfs/validator/api/handler/LoaderConfiguration.java index 5177fab..8506a0a 100644 --- a/gbfs-validator-java-api/src/main/java/org/entur/gbfs/validator/api/handler/LoaderConfiguration.java +++ b/gbfs-validator-java-api/src/main/java/org/entur/gbfs/validator/api/handler/LoaderConfiguration.java @@ -27,15 +27,15 @@ @Configuration public class LoaderConfiguration { - @Bean - public Loader loader(LoaderProperties properties) { - return new Loader( - properties.getHttp().getMaxTotalConnections(), - properties.getHttp().getMaxConnectionsPerRoute(), - properties.getHttp().getConnectTimeoutSeconds(), - properties.getHttp().getResponseTimeoutSeconds(), - properties.getThreadPool().getSize(), - properties.getHttp().getHeaders() - ); - } + @Bean + public Loader loader(LoaderProperties properties) { + return new Loader( + properties.getHttp().getMaxTotalConnections(), + properties.getHttp().getMaxConnectionsPerRoute(), + properties.getHttp().getConnectTimeoutSeconds(), + properties.getHttp().getResponseTimeoutSeconds(), + properties.getThreadPool().getSize(), + properties.getHttp().getHeaders() + ); + } } diff --git a/gbfs-validator-java-api/src/main/java/org/entur/gbfs/validator/api/handler/LoaderProperties.java b/gbfs-validator-java-api/src/main/java/org/entur/gbfs/validator/api/handler/LoaderProperties.java index 66675c2..80cfcfb 100644 --- a/gbfs-validator-java-api/src/main/java/org/entur/gbfs/validator/api/handler/LoaderProperties.java +++ b/gbfs-validator-java-api/src/main/java/org/entur/gbfs/validator/api/handler/LoaderProperties.java @@ -20,92 +20,93 @@ package org.entur.gbfs.validator.api.handler; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; - import java.util.HashMap; import java.util.Map; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; @Component @ConfigurationProperties(prefix = "loader") public class LoaderProperties { - private Http http = new Http(); - private ThreadPool threadPool = new ThreadPool(); + private Http http = new Http(); + private ThreadPool threadPool = new ThreadPool(); - public Http getHttp() { - return http; - } + public Http getHttp() { + return http; + } - public void setHttp(Http http) { - this.http = http; - } + public void setHttp(Http http) { + this.http = http; + } - public ThreadPool getThreadPool() { - return threadPool; - } + public ThreadPool getThreadPool() { + return threadPool; + } - public void setThreadPool(ThreadPool threadPool) { - this.threadPool = threadPool; - } + public void setThreadPool(ThreadPool threadPool) { + this.threadPool = threadPool; + } - public static class Http { - private int maxTotalConnections = 50; - private int maxConnectionsPerRoute = 20; - private int connectTimeoutSeconds = 5; - private int responseTimeoutSeconds = 5; - private Map headers = new HashMap<>(); + public static class Http { - public int getMaxTotalConnections() { - return maxTotalConnections; - } + private int maxTotalConnections = 50; + private int maxConnectionsPerRoute = 20; + private int connectTimeoutSeconds = 5; + private int responseTimeoutSeconds = 5; + private Map headers = new HashMap<>(); - public void setMaxTotalConnections(int maxTotalConnections) { - this.maxTotalConnections = maxTotalConnections; - } + public int getMaxTotalConnections() { + return maxTotalConnections; + } - public int getMaxConnectionsPerRoute() { - return maxConnectionsPerRoute; - } + public void setMaxTotalConnections(int maxTotalConnections) { + this.maxTotalConnections = maxTotalConnections; + } - public void setMaxConnectionsPerRoute(int maxConnectionsPerRoute) { - this.maxConnectionsPerRoute = maxConnectionsPerRoute; - } + public int getMaxConnectionsPerRoute() { + return maxConnectionsPerRoute; + } - public int getConnectTimeoutSeconds() { - return connectTimeoutSeconds; - } + public void setMaxConnectionsPerRoute(int maxConnectionsPerRoute) { + this.maxConnectionsPerRoute = maxConnectionsPerRoute; + } - public void setConnectTimeoutSeconds(int connectTimeoutSeconds) { - this.connectTimeoutSeconds = connectTimeoutSeconds; - } + public int getConnectTimeoutSeconds() { + return connectTimeoutSeconds; + } + + public void setConnectTimeoutSeconds(int connectTimeoutSeconds) { + this.connectTimeoutSeconds = connectTimeoutSeconds; + } - public int getResponseTimeoutSeconds() { - return responseTimeoutSeconds; - } + public int getResponseTimeoutSeconds() { + return responseTimeoutSeconds; + } - public void setResponseTimeoutSeconds(int responseTimeoutSeconds) { - this.responseTimeoutSeconds = responseTimeoutSeconds; - } + public void setResponseTimeoutSeconds(int responseTimeoutSeconds) { + this.responseTimeoutSeconds = responseTimeoutSeconds; + } - public Map getHeaders() { - return headers; - } + public Map getHeaders() { + return headers; + } - public void setHeaders(Map headers) { - this.headers = headers; - } + public void setHeaders(Map headers) { + this.headers = headers; } + } - public static class ThreadPool { - private int size = 20; + public static class ThreadPool { - public int getSize() { - return size; - } + private int size = 20; + + public int getSize() { + return size; + } - public void setSize(int size) { - this.size = size; - } + public void setSize(int size) { + this.size = size; } + } } diff --git a/gbfs-validator-java-api/src/main/java/org/entur/gbfs/validator/api/handler/SecurityConfig.java b/gbfs-validator-java-api/src/main/java/org/entur/gbfs/validator/api/handler/SecurityConfig.java index d658f01..57b9dd3 100644 --- a/gbfs-validator-java-api/src/main/java/org/entur/gbfs/validator/api/handler/SecurityConfig.java +++ b/gbfs-validator-java-api/src/main/java/org/entur/gbfs/validator/api/handler/SecurityConfig.java @@ -20,6 +20,8 @@ package org.entur.gbfs.validator.api.handler; +import java.util.Arrays; +import java.util.List; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; @@ -31,9 +33,6 @@ import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; -import java.util.Arrays; -import java.util.List; - /** * Security configuration for the GBFS Validator API. * Handles CORS configuration to allow cross-origin requests. @@ -42,33 +41,34 @@ @EnableWebSecurity public class SecurityConfig { - @Bean - public SecurityFilterChain filterChain( - HttpSecurity http - ) throws Exception { - return http - .csrf(csrf -> csrf.disable()) - .cors(cors -> cors.configurationSource(corsConfigurationSource())) - .build(); - } + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + return http + .csrf(csrf -> csrf.disable()) + .cors(cors -> cors.configurationSource(corsConfigurationSource())) + .build(); + } - private CorsConfigurationSource corsConfigurationSource() { - CorsConfiguration configuration = new CorsConfiguration(); - configuration.addAllowedOriginPattern("*"); - configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); - configuration.setAllowedHeaders(List.of("*")); - configuration.setExposedHeaders(List.of("*")); - configuration.setAllowCredentials(false); - configuration.setMaxAge(3600L); // Cache preflight response for 1 hour - UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - source.registerCorsConfiguration("/**", configuration); - return source; - } + private CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.addAllowedOriginPattern("*"); + configuration.setAllowedMethods( + Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS") + ); + configuration.setAllowedHeaders(List.of("*")); + configuration.setExposedHeaders(List.of("*")); + configuration.setAllowCredentials(false); + configuration.setMaxAge(3600L); // Cache preflight response for 1 hour + UrlBasedCorsConfigurationSource source = + new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } - @Bean - public AuthenticationManager noopAuthenticationManager() { - return authentication -> { - throw new AuthenticationServiceException("Authentication is disabled"); - }; - } + @Bean + public AuthenticationManager noopAuthenticationManager() { + return authentication -> { + throw new AuthenticationServiceException("Authentication is disabled"); + }; + } } diff --git a/gbfs-validator-java-api/src/main/java/org/entur/gbfs/validator/api/handler/ValidateApiDelegateHandler.java b/gbfs-validator-java-api/src/main/java/org/entur/gbfs/validator/api/handler/ValidateApiDelegateHandler.java index 9f623d3..d32311c 100644 --- a/gbfs-validator-java-api/src/main/java/org/entur/gbfs/validator/api/handler/ValidateApiDelegateHandler.java +++ b/gbfs-validator-java-api/src/main/java/org/entur/gbfs/validator/api/handler/ValidateApiDelegateHandler.java @@ -23,11 +23,18 @@ import com.google.common.collect.Multimap; import com.google.common.collect.MultimapBuilder; import jakarta.annotation.PreDestroy; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import org.entur.gbfs.validation.GbfsValidator; import org.entur.gbfs.validation.GbfsValidatorFactory; import org.entur.gbfs.validation.model.FileValidationError; import org.entur.gbfs.validation.model.FileValidationResult; import org.entur.gbfs.validation.model.ValidationResult; +import org.entur.gbfs.validation.model.ValidatorError; import org.entur.gbfs.validator.api.gen.ValidateApiDelegate; import org.entur.gbfs.validator.api.model.BasicAuth; import org.entur.gbfs.validator.api.model.BearerTokenAuth; @@ -36,264 +43,336 @@ import org.entur.gbfs.validator.api.model.OAuthClientCredentialsGrantAuth; import org.entur.gbfs.validator.api.model.SystemError; import org.entur.gbfs.validator.api.model.ValidatePostRequest; +import org.entur.gbfs.validator.api.model.ValidatePostRequestAuth; import org.entur.gbfs.validator.api.model.ValidationResultSummary; import org.entur.gbfs.validator.loader.LoadedFile; import org.entur.gbfs.validator.loader.Loader; -import org.entur.gbfs.validator.loader.auth.Authentication; import org.entur.gbfs.validator.loader.LoaderError; -import org.entur.gbfs.validation.model.ValidatorError; +import org.entur.gbfs.validator.loader.auth.Authentication; import org.openapitools.jackson.nullable.JsonNullable; -import org.entur.gbfs.validator.api.model.ValidatePostRequestAuth; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - /** * Service implementation for GBFS validation API operations. * Handles validation requests by loading GBFS files and running them through the validator. */ @Service public class ValidateApiDelegateHandler implements ValidateApiDelegate { - private static final Logger logger = LoggerFactory.getLogger(ValidateApiDelegateHandler.class); - - private final Loader loader; - private final VersionProvider versionProvider; - - /** - * Creates a new validation handler. - * - * @param loader the GBFS file loader to use - * @param versionProvider provides access to application version information - */ - public ValidateApiDelegateHandler(Loader loader, VersionProvider versionProvider) { - this.loader = loader; - this.versionProvider = versionProvider; - } - - /** - * Cleans up resources when the service is destroyed. - * Closes the loader's HTTP client and thread pool. - */ - @PreDestroy - public void destroy() { - try { - if (loader != null) { - loader.close(); - } - } catch (IOException e) { - logger.error("Error closing Loader", e); - } - } - - /** - * Validates a GBFS feed by loading all files and running validation. - * - * @param validatePostRequest the validation request containing feed URL and optional authentication - * @return validation results with file-level errors and system errors - */ - @Override - public ResponseEntity validatePost(ValidatePostRequest validatePostRequest) { - logger.debug("Received request for url: {}", validatePostRequest.getFeedUrl()); - try { - Authentication loaderAuth = null; - ValidatePostRequestAuth apiAuth = validatePostRequest.getAuth(); - - if (apiAuth != null) { - if (apiAuth instanceof BasicAuth basic) { - if (basic.getUsername() != null && basic.getPassword() != null) { - loaderAuth = new org.entur.gbfs.validator.loader.auth.BasicAuth(basic.getUsername(), basic.getPassword()); - } - } else if (apiAuth instanceof BearerTokenAuth bearer) { - if (bearer.getToken() != null) { - loaderAuth = new org.entur.gbfs.validator.loader.auth.BearerTokenAuth(bearer.getToken()); - } - } else if (apiAuth instanceof OAuthClientCredentialsGrantAuth oauth) { - if (oauth.getClientId() != null && oauth.getClientSecret() != null && oauth.getTokenUrl() != null) { - loaderAuth = new org.entur.gbfs.validator.loader.auth.OAuthClientCredentialsGrantAuth(oauth.getClientId(), oauth.getClientSecret(), oauth.getTokenUrl().toString()); - } - } - } - - List allLoadedFiles = loader.load(validatePostRequest.getFeedUrl(), loaderAuth); - - logger.debug("Loaded files: {}", allLoadedFiles.size()); - Multimap filesByLanguage = MultimapBuilder.hashKeys().arrayListValues().build(); - for (LoadedFile loadedFile : allLoadedFiles) { - String langKey = loadedFile.language() != null ? loadedFile.language() : "default_lang"; - filesByLanguage.put(langKey, loadedFile); - } - - GbfsValidator validator = GbfsValidatorFactory.getGbfsJsonValidator(); - List resultsPerLanguage = new ArrayList<>(); - - filesByLanguage.asMap().forEach((languageKey, loadedFilesForLang) -> { - logger.debug("Processing language group: {}", languageKey); - Map validatorInputMap = new HashMap<>(); - List currentLanguageLoadedFiles = new ArrayList<>(loadedFilesForLang); - - for (LoadedFile file : currentLanguageLoadedFiles) { - if (file.fileContents() != null) { - validatorInputMap.put(file.fileName(), file.fileContents()); - } - } - - ValidationResult internalValidationResult = validator.validate(validatorInputMap); - - resultsPerLanguage.add( - mapValidationResult( - internalValidationResult, - currentLanguageLoadedFiles, - "default_lang".equals(languageKey) ? null : languageKey - ) - ); - logger.debug("Processed {} files for language group: {}", currentLanguageLoadedFiles.size(), languageKey); - }); - - return ResponseEntity.ok( - mergeValidationResults(resultsPerLanguage) - ); - - } catch (IOException e) { - logger.error("IOException during validation process", e); - throw new RuntimeException(e); - } + private static final Logger logger = LoggerFactory.getLogger( + ValidateApiDelegateHandler.class + ); + + private final Loader loader; + private final VersionProvider versionProvider; + + /** + * Creates a new validation handler. + * + * @param loader the GBFS file loader to use + * @param versionProvider provides access to application version information + */ + public ValidateApiDelegateHandler( + Loader loader, + VersionProvider versionProvider + ) { + this.loader = loader; + this.versionProvider = versionProvider; + } + + /** + * Cleans up resources when the service is destroyed. + * Closes the loader's HTTP client and thread pool. + */ + @PreDestroy + public void destroy() { + try { + if (loader != null) { + loader.close(); + } + } catch (IOException e) { + logger.error("Error closing Loader", e); } - - private org.entur.gbfs.validator.api.model.ValidationResult mergeValidationResults(List results) { - if (results.isEmpty()) { - org.entur.gbfs.validator.api.model.ValidationResult emptyApiResult = new org.entur.gbfs.validator.api.model.ValidationResult(); - ValidationResultSummary emptySummary = new ValidationResultSummary(); - emptySummary.setValidatorVersion(versionProvider.getVersion()); - emptySummary.setFiles(new ArrayList<>()); - emptyApiResult.setSummary(emptySummary); - return emptyApiResult; + } + + /** + * Validates a GBFS feed by loading all files and running validation. + * + * @param validatePostRequest the validation request containing feed URL and optional authentication + * @return validation results with file-level errors and system errors + */ + @Override + public ResponseEntity validatePost( + ValidatePostRequest validatePostRequest + ) { + logger.debug( + "Received request for url: {}", + validatePostRequest.getFeedUrl() + ); + try { + Authentication loaderAuth = null; + ValidatePostRequestAuth apiAuth = validatePostRequest.getAuth(); + + if (apiAuth != null) { + if (apiAuth instanceof BasicAuth basic) { + if (basic.getUsername() != null && basic.getPassword() != null) { + loaderAuth = + new org.entur.gbfs.validator.loader.auth.BasicAuth( + basic.getUsername(), + basic.getPassword() + ); + } + } else if (apiAuth instanceof BearerTokenAuth bearer) { + if (bearer.getToken() != null) { + loaderAuth = + new org.entur.gbfs.validator.loader.auth.BearerTokenAuth( + bearer.getToken() + ); + } + } else if (apiAuth instanceof OAuthClientCredentialsGrantAuth oauth) { + if ( + oauth.getClientId() != null && + oauth.getClientSecret() != null && + oauth.getTokenUrl() != null + ) { + loaderAuth = + new org.entur.gbfs.validator.loader.auth.OAuthClientCredentialsGrantAuth( + oauth.getClientId(), + oauth.getClientSecret(), + oauth.getTokenUrl().toString() + ); + } } - - org.entur.gbfs.validator.api.model.ValidationResult mergedResult = new org.entur.gbfs.validator.api.model.ValidationResult(); - ValidationResultSummary summary = new ValidationResultSummary(); - summary.setValidatorVersion(results.get(0).getSummary().getValidatorVersion()); - List allFiles = new ArrayList<>(); - results.forEach(result -> { - if (result.getSummary() != null && result.getSummary().getFiles() != null) { - allFiles.addAll(result.getSummary().getFiles()); + } + + List allLoadedFiles = loader.load( + validatePostRequest.getFeedUrl(), + loaderAuth + ); + + logger.debug("Loaded files: {}", allLoadedFiles.size()); + + Multimap filesByLanguage = MultimapBuilder + .hashKeys() + .arrayListValues() + .build(); + for (LoadedFile loadedFile : allLoadedFiles) { + String langKey = loadedFile.language() != null + ? loadedFile.language() + : "default_lang"; + filesByLanguage.put(langKey, loadedFile); + } + + GbfsValidator validator = GbfsValidatorFactory.getGbfsJsonValidator(); + List resultsPerLanguage = + new ArrayList<>(); + + filesByLanguage + .asMap() + .forEach((languageKey, loadedFilesForLang) -> { + logger.debug("Processing language group: {}", languageKey); + Map validatorInputMap = new HashMap<>(); + List currentLanguageLoadedFiles = new ArrayList<>( + loadedFilesForLang + ); + + for (LoadedFile file : currentLanguageLoadedFiles) { + if (file.fileContents() != null) { + validatorInputMap.put(file.fileName(), file.fileContents()); } + } + + ValidationResult internalValidationResult = validator.validate( + validatorInputMap + ); + + resultsPerLanguage.add( + mapValidationResult( + internalValidationResult, + currentLanguageLoadedFiles, + "default_lang".equals(languageKey) ? null : languageKey + ) + ); + logger.debug( + "Processed {} files for language group: {}", + currentLanguageLoadedFiles.size(), + languageKey + ); }); - summary.setFiles(allFiles); - - mergedResult.setSummary(summary); - return mergedResult; + return ResponseEntity.ok(mergeValidationResults(resultsPerLanguage)); + } catch (IOException e) { + logger.error("IOException during validation process", e); + throw new RuntimeException(e); } - - - private org.entur.gbfs.validator.api.model.ValidationResult mapValidationResult( - ValidationResult internalValidationResult, - List loadedFilesForLanguage, - String language - ) { - ValidationResultSummary validationResultSummary = new ValidationResultSummary(); - validationResultSummary.setValidatorVersion(versionProvider.getVersion()); - - validationResultSummary.setFiles( - mapFiles(loadedFilesForLanguage, internalValidationResult.files(), language) - ); - - org.entur.gbfs.validator.api.model.ValidationResult apiResult = new org.entur.gbfs.validator.api.model.ValidationResult(); - apiResult.setSummary(validationResultSummary); - return apiResult; + } + + private org.entur.gbfs.validator.api.model.ValidationResult mergeValidationResults( + List results + ) { + if (results.isEmpty()) { + org.entur.gbfs.validator.api.model.ValidationResult emptyApiResult = + new org.entur.gbfs.validator.api.model.ValidationResult(); + ValidationResultSummary emptySummary = new ValidationResultSummary(); + emptySummary.setValidatorVersion(versionProvider.getVersion()); + emptySummary.setFiles(new ArrayList<>()); + emptyApiResult.setSummary(emptySummary); + return emptyApiResult; } - private List mapFiles( - List loadedFilesForLanguage, - Map validatedFileResultsMap, - String language - ) { - List apiGbfsFiles = new ArrayList<>(); - - for (LoadedFile loadedFile : loadedFilesForLanguage) { - GbfsFile apiFile = new GbfsFile(); - apiFile.setName(loadedFile.fileName()); - apiFile.setUrl(loadedFile.url()); - - List combinedApiSystemErrors = new ArrayList<>(); - - List loaderSystemErrors = loadedFile.loaderErrors(); - if (loaderSystemErrors != null && !loaderSystemErrors.isEmpty()) { - combinedApiSystemErrors.addAll(mapLoaderSystemErrorsToApi(loaderSystemErrors)); - } - - FileValidationResult validationResult = validatedFileResultsMap.get(loadedFile.fileName()); - - if (validationResult != null) { - apiFile.setSchema(validationResult.schema()); - apiFile.setVersion(validationResult.version()); - apiFile.setErrors(mapFileValidationErrors(validationResult.errors())); - - List validatorSystemErrors = validationResult.validatorErrors(); - if (validatorSystemErrors != null && !validatorSystemErrors.isEmpty()) { - combinedApiSystemErrors.addAll(mapValidatorSystemErrorsToApi(validatorSystemErrors)); - } - } else { - apiFile.setErrors(new ArrayList<>()); - } - - apiFile.setSystemErrors(combinedApiSystemErrors); - - if (loadedFile.fileName().equals("gbfs.json") || loadedFile.fileName().equals("gbfs")) { - apiFile.setLanguage(null); - } else { - apiFile.setLanguage(JsonNullable.of(language)); - } - apiGbfsFiles.add(apiFile); + org.entur.gbfs.validator.api.model.ValidationResult mergedResult = + new org.entur.gbfs.validator.api.model.ValidationResult(); + ValidationResultSummary summary = new ValidationResultSummary(); + summary.setValidatorVersion( + results.get(0).getSummary().getValidatorVersion() + ); + List allFiles = new ArrayList<>(); + results.forEach(result -> { + if ( + result.getSummary() != null && result.getSummary().getFiles() != null + ) { + allFiles.addAll(result.getSummary().getFiles()); + } + }); + + summary.setFiles(allFiles); + + mergedResult.setSummary(summary); + return mergedResult; + } + + private org.entur.gbfs.validator.api.model.ValidationResult mapValidationResult( + ValidationResult internalValidationResult, + List loadedFilesForLanguage, + String language + ) { + ValidationResultSummary validationResultSummary = + new ValidationResultSummary(); + validationResultSummary.setValidatorVersion(versionProvider.getVersion()); + + validationResultSummary.setFiles( + mapFiles( + loadedFilesForLanguage, + internalValidationResult.files(), + language + ) + ); + + org.entur.gbfs.validator.api.model.ValidationResult apiResult = + new org.entur.gbfs.validator.api.model.ValidationResult(); + apiResult.setSummary(validationResultSummary); + return apiResult; + } + + private List mapFiles( + List loadedFilesForLanguage, + Map validatedFileResultsMap, + String language + ) { + List apiGbfsFiles = new ArrayList<>(); + + for (LoadedFile loadedFile : loadedFilesForLanguage) { + GbfsFile apiFile = new GbfsFile(); + apiFile.setName(loadedFile.fileName()); + apiFile.setUrl(loadedFile.url()); + + List combinedApiSystemErrors = new ArrayList<>(); + + List loaderSystemErrors = loadedFile.loaderErrors(); + if (loaderSystemErrors != null && !loaderSystemErrors.isEmpty()) { + combinedApiSystemErrors.addAll( + mapLoaderSystemErrorsToApi(loaderSystemErrors) + ); + } + + FileValidationResult validationResult = validatedFileResultsMap.get( + loadedFile.fileName() + ); + + if (validationResult != null) { + apiFile.setSchema(validationResult.schema()); + apiFile.setVersion(validationResult.version()); + apiFile.setErrors(mapFileValidationErrors(validationResult.errors())); + + List validatorSystemErrors = + validationResult.validatorErrors(); + if (validatorSystemErrors != null && !validatorSystemErrors.isEmpty()) { + combinedApiSystemErrors.addAll( + mapValidatorSystemErrorsToApi(validatorSystemErrors) + ); } - return apiGbfsFiles; + } else { + apiFile.setErrors(new ArrayList<>()); + } + + apiFile.setSystemErrors(combinedApiSystemErrors); + + if ( + loadedFile.fileName().equals("gbfs.json") || + loadedFile.fileName().equals("gbfs") + ) { + apiFile.setLanguage(null); + } else { + apiFile.setLanguage(JsonNullable.of(language)); + } + apiGbfsFiles.add(apiFile); } - - private List mapLoaderSystemErrorsToApi(List loaderSystemErrors) { - if (loaderSystemErrors == null) { - return new ArrayList<>(); - } - return loaderSystemErrors.stream().map(loaderError -> { - SystemError apiError = new SystemError(); - apiError.setError(loaderError.error()); - apiError.setMessage(loaderError.message()); - return apiError; - }).toList(); + return apiGbfsFiles; + } + + private List mapLoaderSystemErrorsToApi( + List loaderSystemErrors + ) { + if (loaderSystemErrors == null) { + return new ArrayList<>(); } - - private List mapValidatorSystemErrorsToApi(List validatorSystemErrors) { - if (validatorSystemErrors == null) { - return new ArrayList<>(); - } - return validatorSystemErrors.stream().map(validatorError -> { - SystemError apiError = new SystemError(); - apiError.setError(validatorError.error()); - apiError.setMessage(validatorError.message()); - return apiError; - }).toList(); + return loaderSystemErrors + .stream() + .map(loaderError -> { + SystemError apiError = new SystemError(); + apiError.setError(loaderError.error()); + apiError.setMessage(loaderError.message()); + return apiError; + }) + .toList(); + } + + private List mapValidatorSystemErrorsToApi( + List validatorSystemErrors + ) { + if (validatorSystemErrors == null) { + return new ArrayList<>(); } - - private List mapFileValidationErrors(List errors) { - if (errors == null) { - return new ArrayList<>(); - } - return errors.stream().map(error -> { - var mapped = new FileError(); - mapped.setMessage(error.message()); - mapped.setInstancePath(error.violationPath()); - mapped.setSchemaPath(error.schemaPath()); - mapped.setKeyword(error.keyword()); - return mapped; - }).toList(); + return validatorSystemErrors + .stream() + .map(validatorError -> { + SystemError apiError = new SystemError(); + apiError.setError(validatorError.error()); + apiError.setMessage(validatorError.message()); + return apiError; + }) + .toList(); + } + + private List mapFileValidationErrors( + List errors + ) { + if (errors == null) { + return new ArrayList<>(); } + return errors + .stream() + .map(error -> { + var mapped = new FileError(); + mapped.setMessage(error.message()); + mapped.setInstancePath(error.violationPath()); + mapped.setSchemaPath(error.schemaPath()); + mapped.setKeyword(error.keyword()); + return mapped; + }) + .toList(); + } } diff --git a/gbfs-validator-java-api/src/main/java/org/entur/gbfs/validator/api/handler/VersionProvider.java b/gbfs-validator-java-api/src/main/java/org/entur/gbfs/validator/api/handler/VersionProvider.java index 4bfd051..73c6935 100644 --- a/gbfs-validator-java-api/src/main/java/org/entur/gbfs/validator/api/handler/VersionProvider.java +++ b/gbfs-validator-java-api/src/main/java/org/entur/gbfs/validator/api/handler/VersionProvider.java @@ -30,13 +30,13 @@ @Component public class VersionProvider { - private final BuildProperties buildProperties; + private final BuildProperties buildProperties; - public VersionProvider(BuildProperties buildProperties) { - this.buildProperties = buildProperties; - } + public VersionProvider(BuildProperties buildProperties) { + this.buildProperties = buildProperties; + } - public String getVersion() { - return buildProperties.getVersion(); - } + public String getVersion() { + return buildProperties.getVersion(); + } } diff --git a/gbfs-validator-java-api/src/test/java/org/entur/gbfs/validator/api/TestApplication.java b/gbfs-validator-java-api/src/test/java/org/entur/gbfs/validator/api/TestApplication.java index 721909e..074dd73 100644 --- a/gbfs-validator-java-api/src/test/java/org/entur/gbfs/validator/api/TestApplication.java +++ b/gbfs-validator-java-api/src/test/java/org/entur/gbfs/validator/api/TestApplication.java @@ -25,7 +25,8 @@ @SpringBootApplication public class TestApplication { - public static void main(String[] args) { - SpringApplication.run(TestApplication.class, args); - } + + public static void main(String[] args) { + SpringApplication.run(TestApplication.class, args); + } } diff --git a/gbfs-validator-java-api/src/test/java/org/entur/gbfs/validator/api/ValidateIntegrationTest.java b/gbfs-validator-java-api/src/test/java/org/entur/gbfs/validator/api/ValidateIntegrationTest.java index 3e0694d..b847fe3 100644 --- a/gbfs-validator-java-api/src/test/java/org/entur/gbfs/validator/api/ValidateIntegrationTest.java +++ b/gbfs-validator-java-api/src/test/java/org/entur/gbfs/validator/api/ValidateIntegrationTest.java @@ -1,5 +1,9 @@ package org.entur.gbfs.validator.api; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + import com.fasterxml.jackson.databind.ObjectMapper; import org.entur.gbfs.validator.api.model.*; import org.junit.jupiter.api.Test; @@ -9,111 +13,123 @@ import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - @SpringBootTest(classes = TestApplication.class) @AutoConfigureMockMvc public class ValidateIntegrationTest { - @Autowired - private MockMvc mockMvc; - - @Autowired - private ObjectMapper objectMapper; - - @Test - void testValidate_NoAuth_Success() throws Exception { - // Create request with no auth - ValidatePostRequest request = new ValidatePostRequest(); - request.setFeedUrl("http://example.com/gbfs.json"); - - // Perform the test - mockMvc.perform(post("/validate") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.summary").exists()); - } - - @Test - void testValidate_BasicAuth_Success() throws Exception { - // Create request with basic auth - ValidatePostRequest request = new ValidatePostRequest(); - request.setFeedUrl("http://example.com/gbfs.json"); - - BasicAuth basicAuth = new BasicAuth(); - basicAuth.setAuthType("basic"); - basicAuth.setUsername("user"); - basicAuth.setPassword("pass"); - request.setAuth(basicAuth); - - // Perform the test - mockMvc.perform(post("/validate") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.summary").exists()); - } - - @Test - void testValidate_BearerTokenAuth_Success() throws Exception { - // Create request with bearer token auth - ValidatePostRequest request = new ValidatePostRequest(); - request.setFeedUrl("http://example.com/gbfs.json"); - - BearerTokenAuth bearerAuth = new BearerTokenAuth(); - bearerAuth.setAuthType("bearer"); - bearerAuth.setToken("token123"); - request.setAuth(bearerAuth); - - // Perform the test - mockMvc.perform(post("/validate") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.summary").exists()); - } - - @Test - void testValidate_OAuthClientCredentials_Success() throws Exception { - // Create request with OAuth client credentials - ValidatePostRequest request = new ValidatePostRequest(); - request.setFeedUrl("http://example.com/gbfs.json"); - - OAuthClientCredentialsGrantAuth oauthAuth = new OAuthClientCredentialsGrantAuth(); - oauthAuth.setAuthType("oauth"); - oauthAuth.setTokenUrl("https://auth.example.com/token"); - oauthAuth.setClientId("client_id"); - oauthAuth.setClientSecret("client_secret"); - request.setAuth(oauthAuth); - - // Perform the test - mockMvc.perform(post("/validate") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.summary").exists()); - } - - @Test - void testValidate_AuthFailure() throws Exception { - // Create request with invalid auth - ValidatePostRequest request = new ValidatePostRequest(); - request.setFeedUrl("http://example.com/gbfs.json"); - - BasicAuth basicAuth = new BasicAuth(); - basicAuth.setAuthType("basic"); - basicAuth.setUsername("wrong_user"); - basicAuth.setPassword("wrong_password"); - request.setAuth(basicAuth); - - // Perform the test - we expect a 200 response with error details in the summary - mockMvc.perform(post("/validate") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.summary.files[0].systemErrors").isNotEmpty()); - } + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @Test + void testValidate_NoAuth_Success() throws Exception { + // Create request with no auth + ValidatePostRequest request = new ValidatePostRequest(); + request.setFeedUrl("http://example.com/gbfs.json"); + + // Perform the test + mockMvc + .perform( + post("/validate") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.summary").exists()); + } + + @Test + void testValidate_BasicAuth_Success() throws Exception { + // Create request with basic auth + ValidatePostRequest request = new ValidatePostRequest(); + request.setFeedUrl("http://example.com/gbfs.json"); + + BasicAuth basicAuth = new BasicAuth(); + basicAuth.setAuthType("basic"); + basicAuth.setUsername("user"); + basicAuth.setPassword("pass"); + request.setAuth(basicAuth); + + // Perform the test + mockMvc + .perform( + post("/validate") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.summary").exists()); + } + + @Test + void testValidate_BearerTokenAuth_Success() throws Exception { + // Create request with bearer token auth + ValidatePostRequest request = new ValidatePostRequest(); + request.setFeedUrl("http://example.com/gbfs.json"); + + BearerTokenAuth bearerAuth = new BearerTokenAuth(); + bearerAuth.setAuthType("bearer"); + bearerAuth.setToken("token123"); + request.setAuth(bearerAuth); + + // Perform the test + mockMvc + .perform( + post("/validate") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.summary").exists()); + } + + @Test + void testValidate_OAuthClientCredentials_Success() throws Exception { + // Create request with OAuth client credentials + ValidatePostRequest request = new ValidatePostRequest(); + request.setFeedUrl("http://example.com/gbfs.json"); + + OAuthClientCredentialsGrantAuth oauthAuth = + new OAuthClientCredentialsGrantAuth(); + oauthAuth.setAuthType("oauth"); + oauthAuth.setTokenUrl("https://auth.example.com/token"); + oauthAuth.setClientId("client_id"); + oauthAuth.setClientSecret("client_secret"); + request.setAuth(oauthAuth); + + // Perform the test + mockMvc + .perform( + post("/validate") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.summary").exists()); + } + + @Test + void testValidate_AuthFailure() throws Exception { + // Create request with invalid auth + ValidatePostRequest request = new ValidatePostRequest(); + request.setFeedUrl("http://example.com/gbfs.json"); + + BasicAuth basicAuth = new BasicAuth(); + basicAuth.setAuthType("basic"); + basicAuth.setUsername("wrong_user"); + basicAuth.setPassword("wrong_password"); + request.setAuth(basicAuth); + + // Perform the test - we expect a 200 response with error details in the summary + mockMvc + .perform( + post("/validate") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.summary.files[0].systemErrors").isNotEmpty()); + } } diff --git a/gbfs-validator-java-loader/src/main/java/org/entur/gbfs/validator/loader/LoadedFile.java b/gbfs-validator-java-loader/src/main/java/org/entur/gbfs/validator/loader/LoadedFile.java index 991c546..e84601a 100644 --- a/gbfs-validator-java-loader/src/main/java/org/entur/gbfs/validator/loader/LoadedFile.java +++ b/gbfs-validator-java-loader/src/main/java/org/entur/gbfs/validator/loader/LoadedFile.java @@ -25,17 +25,22 @@ import java.util.List; public record LoadedFile( - String fileName, - String url, - InputStream fileContents, - String language, - List loaderErrors + String fileName, + String url, + InputStream fileContents, + String language, + List loaderErrors ) { - public LoadedFile(String fileName, String url, InputStream fileContents, String language) { - this(fileName, url, fileContents, language, new ArrayList<>()); - } + public LoadedFile( + String fileName, + String url, + InputStream fileContents, + String language + ) { + this(fileName, url, fileContents, language, new ArrayList<>()); + } - public LoadedFile(String fileName, String url, InputStream fileContents) { - this(fileName, url, fileContents, null, new ArrayList<>()); - } + public LoadedFile(String fileName, String url, InputStream fileContents) { + this(fileName, url, fileContents, null, new ArrayList<>()); + } } diff --git a/gbfs-validator-java-loader/src/main/java/org/entur/gbfs/validator/loader/Loader.java b/gbfs-validator-java-loader/src/main/java/org/entur/gbfs/validator/loader/Loader.java index 252e659..64e7b30 100644 --- a/gbfs-validator-java-loader/src/main/java/org/entur/gbfs/validator/loader/Loader.java +++ b/gbfs-validator-java-loader/src/main/java/org/entur/gbfs/validator/loader/Loader.java @@ -20,25 +20,6 @@ package org.entur.gbfs.validator.loader; -import org.apache.hc.client5.http.classic.methods.HttpGet; -import org.apache.hc.client5.http.config.RequestConfig; -import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; -import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; -import org.apache.hc.client5.http.impl.classic.HttpClients; -import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; -import org.apache.hc.client5.http.classic.methods.HttpPost; -import org.apache.hc.core5.http.HttpHeaders; -import org.apache.hc.core5.http.ParseException; -import org.apache.hc.core5.http.io.entity.EntityUtils; -import org.apache.hc.core5.http.io.entity.StringEntity; -import org.apache.hc.core5.util.Timeout; -import org.entur.gbfs.validator.loader.auth.Authentication; -import org.entur.gbfs.validator.loader.auth.BasicAuth; -import org.entur.gbfs.validator.loader.auth.BearerTokenAuth; -import org.entur.gbfs.validator.loader.auth.OAuthClientCredentialsGrantAuth; -import org.json.JSONObject; -import org.json.JSONTokener; - import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; @@ -57,6 +38,24 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; +import org.apache.hc.client5.http.classic.methods.HttpGet; +import org.apache.hc.client5.http.classic.methods.HttpPost; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; +import org.apache.hc.client5.http.impl.classic.HttpClients; +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; +import org.apache.hc.core5.http.HttpHeaders; +import org.apache.hc.core5.http.ParseException; +import org.apache.hc.core5.http.io.entity.EntityUtils; +import org.apache.hc.core5.http.io.entity.StringEntity; +import org.apache.hc.core5.util.Timeout; +import org.entur.gbfs.validator.loader.auth.Authentication; +import org.entur.gbfs.validator.loader.auth.BasicAuth; +import org.entur.gbfs.validator.loader.auth.BearerTokenAuth; +import org.entur.gbfs.validator.loader.auth.OAuthClientCredentialsGrantAuth; +import org.json.JSONObject; +import org.json.JSONTokener; /** * Loads GBFS (General Bikeshare Feed Specification) files from HTTP/HTTPS URLs or local file system. @@ -64,273 +63,380 @@ * Thread-safe and designed to be used as a singleton bean. */ public class Loader { - private final CloseableHttpClient httpClient; - private final ExecutorService executorService; - private final Map customHeaders; - - private String getFileName(URI uri) { - String path = uri.getPath(); - if (path == null || path.isEmpty()) { - return "unknown_file"; - } - return new File(path).getName(); - } - - /** - * Creates a Loader with default configuration. - * Uses 50 max total connections, 20 max per route, 5 second timeouts, 20 threads, and no custom headers. - */ - public Loader() { - this(50, 20, 5, 5, 20, Collections.emptyMap()); - } - - /** - * Creates a Loader with custom configuration. - * - * @param maxTotalConnections maximum number of total HTTP connections in the pool - * @param maxConnectionsPerRoute maximum number of connections per route - * @param connectTimeoutSeconds connection timeout in seconds - * @param responseTimeoutSeconds response timeout in seconds - * @param threadPoolSize number of threads for parallel loading - * @param customHeaders custom HTTP headers to include in all requests - */ - public Loader(int maxTotalConnections, int maxConnectionsPerRoute, - int connectTimeoutSeconds, int responseTimeoutSeconds, - int threadPoolSize, Map customHeaders) { - this.customHeaders = customHeaders != null ? customHeaders : new HashMap<>(); - - PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(); - connectionManager.setMaxTotal(maxTotalConnections); - connectionManager.setDefaultMaxPerRoute(maxConnectionsPerRoute); - - RequestConfig requestConfig = RequestConfig.custom() - .setConnectTimeout(Timeout.of(connectTimeoutSeconds, TimeUnit.SECONDS)) - .setResponseTimeout(Timeout.of(responseTimeoutSeconds, TimeUnit.SECONDS)) - .build(); - - httpClient = HttpClients.custom() - .setConnectionManager(connectionManager) - .setDefaultRequestConfig(requestConfig) - .build(); - - executorService = Executors.newFixedThreadPool(threadPoolSize); - } - - /** - * Loads GBFS files from the given discovery file URL without authentication. - * - * @param discoveryURIString URL or file path to the GBFS discovery file - * @return list of loaded files with their content and metadata - * @throws IOException if an error occurs during loading - */ - public List load(String discoveryURIString) throws IOException { - return load(discoveryURIString, null); - } - - /** - * Loads GBFS files from the given discovery file URL with authentication. - * - * @param discoveryURIString URL or file path to the GBFS discovery file - * @param auth authentication credentials for protected feeds, or null for public feeds - * @return list of loaded files with their content and metadata - * @throws IOException if an error occurs during loading - */ - public List load(String discoveryURIString, Authentication auth) throws IOException { - URI discoveryURI = URI.create(discoveryURIString); - LoadedFile discoveryLoadedFile = loadFile(discoveryURI, auth); - - if (discoveryLoadedFile.fileContents() == null) { - List loadedFiles = new ArrayList<>(); - loadedFiles.add(discoveryLoadedFile); - return loadedFiles; - } - - ByteArrayOutputStream discoveryFileCopy = new ByteArrayOutputStream(); - org.apache.commons.io.IOUtils.copy(discoveryLoadedFile.fileContents(), discoveryFileCopy); - byte[] discoveryFileBytes = discoveryFileCopy.toByteArray(); - discoveryLoadedFile.fileContents().close(); - - JSONObject discoveryFileJson = new JSONObject(new JSONTokener(new ByteArrayInputStream(discoveryFileBytes))); - String version = discoveryFileJson.getString("version"); - - List loadedFiles = new ArrayList<>(); - loadedFiles.add(new LoadedFile( - discoveryLoadedFile.fileName(), - discoveryLoadedFile.url(), - new ByteArrayInputStream(discoveryFileBytes), - discoveryLoadedFile.language(), - discoveryLoadedFile.loaderErrors() - )); - - if (version.matches("^3\\.\\d")) { - loadedFiles.addAll(getV3Files(discoveryFileJson, loadedFiles.get(0), auth)); - } else { - loadedFiles.addAll(getPreV3Files(discoveryFileJson, loadedFiles.get(0), auth)); - } + private final CloseableHttpClient httpClient; + private final ExecutorService executorService; + private final Map customHeaders; - return loadedFiles; + private String getFileName(URI uri) { + String path = uri.getPath(); + if (path == null || path.isEmpty()) { + return "unknown_file"; } - - private List getV3Files(JSONObject discoveryFileJson, LoadedFile gbfsLoadedFile, Authentication auth) { - List loadedFeedFiles = new ArrayList<>(); - - List> futures = discoveryFileJson.getJSONObject("data").getJSONArray("feeds").toList().stream() - .map(feed -> { - @SuppressWarnings("unchecked") - Map feedMap = (Map) feed; - String url = (String) feedMap.get("url"); - String name = (String) feedMap.get("name"); - - return CompletableFuture.supplyAsync(() -> { - LoadedFile loadedFile = loadFile(URI.create(url), auth); - return new LoadedFile(name, url, loadedFile.fileContents(), loadedFile.language(), loadedFile.loaderErrors()); - }, executorService); - }) - .toList(); - loadedFeedFiles.addAll(futures.stream() - .map(CompletableFuture::join) - .toList()); - - return loadedFeedFiles; + return new File(path).getName(); + } + + /** + * Creates a Loader with default configuration. + * Uses 50 max total connections, 20 max per route, 5 second timeouts, 20 threads, and no custom headers. + */ + public Loader() { + this(50, 20, 5, 5, 20, Collections.emptyMap()); + } + + /** + * Creates a Loader with custom configuration. + * + * @param maxTotalConnections maximum number of total HTTP connections in the pool + * @param maxConnectionsPerRoute maximum number of connections per route + * @param connectTimeoutSeconds connection timeout in seconds + * @param responseTimeoutSeconds response timeout in seconds + * @param threadPoolSize number of threads for parallel loading + * @param customHeaders custom HTTP headers to include in all requests + */ + public Loader( + int maxTotalConnections, + int maxConnectionsPerRoute, + int connectTimeoutSeconds, + int responseTimeoutSeconds, + int threadPoolSize, + Map customHeaders + ) { + this.customHeaders = + customHeaders != null ? customHeaders : new HashMap<>(); + + PoolingHttpClientConnectionManager connectionManager = + new PoolingHttpClientConnectionManager(); + connectionManager.setMaxTotal(maxTotalConnections); + connectionManager.setDefaultMaxPerRoute(maxConnectionsPerRoute); + + RequestConfig requestConfig = RequestConfig + .custom() + .setConnectTimeout(Timeout.of(connectTimeoutSeconds, TimeUnit.SECONDS)) + .setResponseTimeout(Timeout.of(responseTimeoutSeconds, TimeUnit.SECONDS)) + .build(); + + httpClient = + HttpClients + .custom() + .setConnectionManager(connectionManager) + .setDefaultRequestConfig(requestConfig) + .build(); + + executorService = Executors.newFixedThreadPool(threadPoolSize); + } + + /** + * Loads GBFS files from the given discovery file URL without authentication. + * + * @param discoveryURIString URL or file path to the GBFS discovery file + * @return list of loaded files with their content and metadata + * @throws IOException if an error occurs during loading + */ + public List load(String discoveryURIString) throws IOException { + return load(discoveryURIString, null); + } + + /** + * Loads GBFS files from the given discovery file URL with authentication. + * + * @param discoveryURIString URL or file path to the GBFS discovery file + * @param auth authentication credentials for protected feeds, or null for public feeds + * @return list of loaded files with their content and metadata + * @throws IOException if an error occurs during loading + */ + public List load(String discoveryURIString, Authentication auth) + throws IOException { + URI discoveryURI = URI.create(discoveryURIString); + LoadedFile discoveryLoadedFile = loadFile(discoveryURI, auth); + + if (discoveryLoadedFile.fileContents() == null) { + List loadedFiles = new ArrayList<>(); + loadedFiles.add(discoveryLoadedFile); + return loadedFiles; } - private List getPreV3Files(JSONObject discoveryFileJson, LoadedFile gbfsLoadedFile, Authentication auth) { - List loadedFeedFiles = new ArrayList<>(); - List> futures = new ArrayList<>(); - - discoveryFileJson.getJSONObject("data") - .keys() - .forEachRemaining(languageKey -> { - discoveryFileJson.getJSONObject("data").getJSONObject(languageKey).getJSONArray("feeds").toList().forEach(feed -> { - @SuppressWarnings("unchecked") - Map feedMap = (Map) feed; - String url = (String) feedMap.get("url"); - String name = (String) feedMap.get("name"); - - futures.add(CompletableFuture.supplyAsync(() -> { - LoadedFile loadedFile = loadFile(URI.create(url), auth); - return new LoadedFile(name, url, loadedFile.fileContents(), languageKey, loadedFile.loaderErrors()); - }, executorService)); - }); - }); - - loadedFeedFiles.addAll(futures.stream() - .map(CompletableFuture::join) - .toList()); - - return loadedFeedFiles; + ByteArrayOutputStream discoveryFileCopy = new ByteArrayOutputStream(); + org.apache.commons.io.IOUtils.copy( + discoveryLoadedFile.fileContents(), + discoveryFileCopy + ); + byte[] discoveryFileBytes = discoveryFileCopy.toByteArray(); + discoveryLoadedFile.fileContents().close(); + + JSONObject discoveryFileJson = new JSONObject( + new JSONTokener(new ByteArrayInputStream(discoveryFileBytes)) + ); + String version = discoveryFileJson.getString("version"); + + List loadedFiles = new ArrayList<>(); + loadedFiles.add( + new LoadedFile( + discoveryLoadedFile.fileName(), + discoveryLoadedFile.url(), + new ByteArrayInputStream(discoveryFileBytes), + discoveryLoadedFile.language(), + discoveryLoadedFile.loaderErrors() + ) + ); + + if (version.matches("^3\\.\\d")) { + loadedFiles.addAll( + getV3Files(discoveryFileJson, loadedFiles.get(0), auth) + ); + } else { + loadedFiles.addAll( + getPreV3Files(discoveryFileJson, loadedFiles.get(0), auth) + ); } - private LoadedFile loadFile(URI fileURI, Authentication auth) { - String fileName = getFileName(fileURI); - String url = fileURI.toString(); - - if ("file".equals(fileURI.getScheme())) { - try { - InputStream stream = getFileInputStream(fileURI); - return new LoadedFile(fileName, url, stream, null, new ArrayList<>()); - } catch (FileNotFoundException e) { - List errors = new ArrayList<>(); - errors.add(new LoaderError("FILE_NOT_FOUND", e.getMessage())); - return new LoadedFile(fileName, url, null, null, errors); - } - } else if ("https".equals(fileURI.getScheme()) || "http".equals(fileURI.getScheme())) { - try { - InputStream stream = getHTTPInputStream(fileURI, auth); - return new LoadedFile(fileName, url, stream, null, new ArrayList<>()); - } catch (IOException e) { - List errors = new ArrayList<>(); - errors.add(new LoaderError("CONNECTION_ERROR", e.getMessage())); - return new LoadedFile(fileName, url, null, null, errors); - } catch (ParseException e) { // Catch ParseException from getHTTPInputStream - List errors = new ArrayList<>(); - errors.add(new LoaderError("PARSE_ERROR", e.getMessage())); - return new LoadedFile(fileName, url, null, null, errors); - } - } - + return loadedFiles; + } + + private List getV3Files( + JSONObject discoveryFileJson, + LoadedFile gbfsLoadedFile, + Authentication auth + ) { + List loadedFeedFiles = new ArrayList<>(); + + List> futures = discoveryFileJson + .getJSONObject("data") + .getJSONArray("feeds") + .toList() + .stream() + .map(feed -> { + @SuppressWarnings("unchecked") + Map feedMap = (Map) feed; + String url = (String) feedMap.get("url"); + String name = (String) feedMap.get("name"); + + return CompletableFuture.supplyAsync( + () -> { + LoadedFile loadedFile = loadFile(URI.create(url), auth); + return new LoadedFile( + name, + url, + loadedFile.fileContents(), + loadedFile.language(), + loadedFile.loaderErrors() + ); + }, + executorService + ); + }) + .toList(); + loadedFeedFiles.addAll( + futures.stream().map(CompletableFuture::join).toList() + ); + + return loadedFeedFiles; + } + + private List getPreV3Files( + JSONObject discoveryFileJson, + LoadedFile gbfsLoadedFile, + Authentication auth + ) { + List loadedFeedFiles = new ArrayList<>(); + List> futures = new ArrayList<>(); + + discoveryFileJson + .getJSONObject("data") + .keys() + .forEachRemaining(languageKey -> { + discoveryFileJson + .getJSONObject("data") + .getJSONObject(languageKey) + .getJSONArray("feeds") + .toList() + .forEach(feed -> { + @SuppressWarnings("unchecked") + Map feedMap = (Map) feed; + String url = (String) feedMap.get("url"); + String name = (String) feedMap.get("name"); + + futures.add( + CompletableFuture.supplyAsync( + () -> { + LoadedFile loadedFile = loadFile(URI.create(url), auth); + return new LoadedFile( + name, + url, + loadedFile.fileContents(), + languageKey, + loadedFile.loaderErrors() + ); + }, + executorService + ) + ); + }); + }); + + loadedFeedFiles.addAll( + futures.stream().map(CompletableFuture::join).toList() + ); + + return loadedFeedFiles; + } + + private LoadedFile loadFile(URI fileURI, Authentication auth) { + String fileName = getFileName(fileURI); + String url = fileURI.toString(); + + if ("file".equals(fileURI.getScheme())) { + try { + InputStream stream = getFileInputStream(fileURI); + return new LoadedFile(fileName, url, stream, null, new ArrayList<>()); + } catch (FileNotFoundException e) { List errors = new ArrayList<>(); - errors.add(new LoaderError("UNSUPPORTED_SCHEME", "Scheme not supported: " + fileURI.getScheme())); + errors.add(new LoaderError("FILE_NOT_FOUND", e.getMessage())); return new LoadedFile(fileName, url, null, null, errors); + } + } else if ( + "https".equals(fileURI.getScheme()) || "http".equals(fileURI.getScheme()) + ) { + try { + InputStream stream = getHTTPInputStream(fileURI, auth); + return new LoadedFile(fileName, url, stream, null, new ArrayList<>()); + } catch (IOException e) { + List errors = new ArrayList<>(); + errors.add(new LoaderError("CONNECTION_ERROR", e.getMessage())); + return new LoadedFile(fileName, url, null, null, errors); + } catch (ParseException e) { // Catch ParseException from getHTTPInputStream + List errors = new ArrayList<>(); + errors.add(new LoaderError("PARSE_ERROR", e.getMessage())); + return new LoadedFile(fileName, url, null, null, errors); + } } - private static FileInputStream getFileInputStream(URI fileURI) throws FileNotFoundException { - return new FileInputStream(new File(fileURI)); - } - - private InputStream getHTTPInputStream(URI fileURI, Authentication auth) throws IOException, ParseException { - HttpGet httpGet = new HttpGet(fileURI); - - customHeaders.forEach(httpGet::setHeader); - - if (auth != null) { - if (auth instanceof BasicAuth basicAuth) { - String authHeader = "Basic " + Base64.getEncoder().encodeToString((basicAuth.getUsername() + ":" + basicAuth.getPassword()).getBytes()); - httpGet.setHeader(HttpHeaders.AUTHORIZATION, authHeader); - } else if (auth instanceof BearerTokenAuth bearerAuth) { - httpGet.setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + bearerAuth.getToken()); - } else if (auth instanceof OAuthClientCredentialsGrantAuth oauth) { - try { - String token = fetchOAuthToken(oauth.getTokenUrl(), oauth.getClientId(), oauth.getClientSecret()); - httpGet.setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + token); - } catch (Exception e) { - throw new IOException("OAuth token fetch failed: " + e.getMessage(), e); - } - } - } - - try (CloseableHttpResponse response = httpClient.execute(httpGet)) { - if (response.getCode() >= 300) { - EntityUtils.consumeQuietly(response.getEntity()); - throw new IOException("HTTP error fetching file: " + response.getCode() + " " + response.getReasonPhrase()); - } - String content = EntityUtils.toString(response.getEntity()); - return new ByteArrayInputStream(content.getBytes()); + List errors = new ArrayList<>(); + errors.add( + new LoaderError( + "UNSUPPORTED_SCHEME", + "Scheme not supported: " + fileURI.getScheme() + ) + ); + return new LoadedFile(fileName, url, null, null, errors); + } + + private static FileInputStream getFileInputStream(URI fileURI) + throws FileNotFoundException { + return new FileInputStream(new File(fileURI)); + } + + private InputStream getHTTPInputStream(URI fileURI, Authentication auth) + throws IOException, ParseException { + HttpGet httpGet = new HttpGet(fileURI); + + customHeaders.forEach(httpGet::setHeader); + + if (auth != null) { + if (auth instanceof BasicAuth basicAuth) { + String authHeader = + "Basic " + + Base64 + .getEncoder() + .encodeToString( + ( + basicAuth.getUsername() + ":" + basicAuth.getPassword() + ).getBytes() + ); + httpGet.setHeader(HttpHeaders.AUTHORIZATION, authHeader); + } else if (auth instanceof BearerTokenAuth bearerAuth) { + httpGet.setHeader( + HttpHeaders.AUTHORIZATION, + "Bearer " + bearerAuth.getToken() + ); + } else if (auth instanceof OAuthClientCredentialsGrantAuth oauth) { + try { + String token = fetchOAuthToken( + oauth.getTokenUrl(), + oauth.getClientId(), + oauth.getClientSecret() + ); + httpGet.setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + token); + } catch (Exception e) { + throw new IOException( + "OAuth token fetch failed: " + e.getMessage(), + e + ); } + } } - private String fetchOAuthToken(String tokenUrl, String clientId, String clientSecret) throws IOException, ParseException { - HttpPost tokenRequest = new HttpPost(tokenUrl); - tokenRequest.setHeader("Content-Type", "application/x-www-form-urlencoded"); - String body = "grant_type=client_credentials&client_id=" + clientId + "&client_secret=" + clientSecret; - tokenRequest.setEntity(new StringEntity(body)); - - try (CloseableHttpResponse response = httpClient.execute(tokenRequest)) { - if (response.getCode() >= 300) { - EntityUtils.consumeQuietly(response.getEntity()); - throw new IOException("OAuth token request failed: " + response.getCode() + " " + response.getReasonPhrase()); - } - String responseString = EntityUtils.toString(response.getEntity()); - JSONObject jsonResponse = new JSONObject(responseString); - if (!jsonResponse.has("access_token")) { - throw new IOException("OAuth token response did not contain access_token"); - } - return jsonResponse.getString("access_token"); - } + try (CloseableHttpResponse response = httpClient.execute(httpGet)) { + if (response.getCode() >= 300) { + EntityUtils.consumeQuietly(response.getEntity()); + throw new IOException( + "HTTP error fetching file: " + + response.getCode() + + " " + + response.getReasonPhrase() + ); + } + String content = EntityUtils.toString(response.getEntity()); + return new ByteArrayInputStream(content.getBytes()); + } + } + + private String fetchOAuthToken( + String tokenUrl, + String clientId, + String clientSecret + ) throws IOException, ParseException { + HttpPost tokenRequest = new HttpPost(tokenUrl); + tokenRequest.setHeader("Content-Type", "application/x-www-form-urlencoded"); + String body = + "grant_type=client_credentials&client_id=" + + clientId + + "&client_secret=" + + clientSecret; + tokenRequest.setEntity(new StringEntity(body)); + + try (CloseableHttpResponse response = httpClient.execute(tokenRequest)) { + if (response.getCode() >= 300) { + EntityUtils.consumeQuietly(response.getEntity()); + throw new IOException( + "OAuth token request failed: " + + response.getCode() + + " " + + response.getReasonPhrase() + ); + } + String responseString = EntityUtils.toString(response.getEntity()); + JSONObject jsonResponse = new JSONObject(responseString); + if (!jsonResponse.has("access_token")) { + throw new IOException( + "OAuth token response did not contain access_token" + ); + } + return jsonResponse.getString("access_token"); + } + } + + /** + * Closes the HTTP client and shuts down the thread pool. + * Attempts graceful shutdown with a 5-second timeout before forcing termination. + * + * @throws IOException if an error occurs closing the HTTP client + */ + public void close() throws IOException { + if (httpClient != null) { + httpClient.close(); } - /** - * Closes the HTTP client and shuts down the thread pool. - * Attempts graceful shutdown with a 5-second timeout before forcing termination. - * - * @throws IOException if an error occurs closing the HTTP client - */ - public void close() throws IOException { - if (httpClient != null) { - httpClient.close(); - } - - if (executorService != null && !executorService.isShutdown()) { - executorService.shutdown(); - try { - if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) { - executorService.shutdownNow(); - } - } catch (InterruptedException e) { - executorService.shutdownNow(); - Thread.currentThread().interrupt(); - } + if (executorService != null && !executorService.isShutdown()) { + executorService.shutdown(); + try { + if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) { + executorService.shutdownNow(); } + } catch (InterruptedException e) { + executorService.shutdownNow(); + Thread.currentThread().interrupt(); + } } + } } diff --git a/gbfs-validator-java-loader/src/main/java/org/entur/gbfs/validator/loader/LoaderError.java b/gbfs-validator-java-loader/src/main/java/org/entur/gbfs/validator/loader/LoaderError.java index bbcf019..e053b8e 100644 --- a/gbfs-validator-java-loader/src/main/java/org/entur/gbfs/validator/loader/LoaderError.java +++ b/gbfs-validator-java-loader/src/main/java/org/entur/gbfs/validator/loader/LoaderError.java @@ -1,4 +1,3 @@ package org.entur.gbfs.validator.loader; -public record LoaderError(String error, String message) { -} +public record LoaderError(String error, String message) {} diff --git a/gbfs-validator-java-loader/src/main/java/org/entur/gbfs/validator/loader/auth/AuthType.java b/gbfs-validator-java-loader/src/main/java/org/entur/gbfs/validator/loader/auth/AuthType.java index 721fa3e..fd6e1eb 100644 --- a/gbfs-validator-java-loader/src/main/java/org/entur/gbfs/validator/loader/auth/AuthType.java +++ b/gbfs-validator-java-loader/src/main/java/org/entur/gbfs/validator/loader/auth/AuthType.java @@ -21,7 +21,7 @@ package org.entur.gbfs.validator.loader.auth; public enum AuthType { - BASIC, - BEARER_TOKEN, - OAUTH_CLIENT_CREDENTIALS + BASIC, + BEARER_TOKEN, + OAUTH_CLIENT_CREDENTIALS, } diff --git a/gbfs-validator-java-loader/src/main/java/org/entur/gbfs/validator/loader/auth/BasicAuth.java b/gbfs-validator-java-loader/src/main/java/org/entur/gbfs/validator/loader/auth/BasicAuth.java index 2fb472a..cf76f85 100644 --- a/gbfs-validator-java-loader/src/main/java/org/entur/gbfs/validator/loader/auth/BasicAuth.java +++ b/gbfs-validator-java-loader/src/main/java/org/entur/gbfs/validator/loader/auth/BasicAuth.java @@ -21,19 +21,20 @@ package org.entur.gbfs.validator.loader.auth; public class BasicAuth implements Authentication { - private final String username; - private final String password; - public BasicAuth(String username, String password) { - this.username = username; - this.password = password; - } + private final String username; + private final String password; - public String getUsername() { - return username; - } + public BasicAuth(String username, String password) { + this.username = username; + this.password = password; + } - public String getPassword() { - return password; - } + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } } diff --git a/gbfs-validator-java-loader/src/main/java/org/entur/gbfs/validator/loader/auth/BearerTokenAuth.java b/gbfs-validator-java-loader/src/main/java/org/entur/gbfs/validator/loader/auth/BearerTokenAuth.java index 4ffd5b5..7e9a431 100644 --- a/gbfs-validator-java-loader/src/main/java/org/entur/gbfs/validator/loader/auth/BearerTokenAuth.java +++ b/gbfs-validator-java-loader/src/main/java/org/entur/gbfs/validator/loader/auth/BearerTokenAuth.java @@ -21,13 +21,14 @@ package org.entur.gbfs.validator.loader.auth; public class BearerTokenAuth implements Authentication { - private final String token; - public BearerTokenAuth(String token) { - this.token = token; - } + private final String token; - public String getToken() { - return token; - } + public BearerTokenAuth(String token) { + this.token = token; + } + + public String getToken() { + return token; + } } diff --git a/gbfs-validator-java-loader/src/main/java/org/entur/gbfs/validator/loader/auth/OAuthClientCredentialsGrantAuth.java b/gbfs-validator-java-loader/src/main/java/org/entur/gbfs/validator/loader/auth/OAuthClientCredentialsGrantAuth.java index b648cc4..1f5b96c 100644 --- a/gbfs-validator-java-loader/src/main/java/org/entur/gbfs/validator/loader/auth/OAuthClientCredentialsGrantAuth.java +++ b/gbfs-validator-java-loader/src/main/java/org/entur/gbfs/validator/loader/auth/OAuthClientCredentialsGrantAuth.java @@ -21,25 +21,30 @@ package org.entur.gbfs.validator.loader.auth; public class OAuthClientCredentialsGrantAuth implements Authentication { - private final String clientId; - private final String clientSecret; - private final String tokenUrl; - public OAuthClientCredentialsGrantAuth(String clientId, String clientSecret, String tokenUrl) { - this.clientId = clientId; - this.clientSecret = clientSecret; - this.tokenUrl = tokenUrl; - } + private final String clientId; + private final String clientSecret; + private final String tokenUrl; - public String getClientId() { - return clientId; - } + public OAuthClientCredentialsGrantAuth( + String clientId, + String clientSecret, + String tokenUrl + ) { + this.clientId = clientId; + this.clientSecret = clientSecret; + this.tokenUrl = tokenUrl; + } - public String getClientSecret() { - return clientSecret; - } + public String getClientId() { + return clientId; + } - public String getTokenUrl() { - return tokenUrl; - } + public String getClientSecret() { + return clientSecret; + } + + public String getTokenUrl() { + return tokenUrl; + } } diff --git a/gbfs-validator-java-loader/src/test/java/org/entur/gbfs/validator/loader/LoaderTest.java b/gbfs-validator-java-loader/src/test/java/org/entur/gbfs/validator/loader/LoaderTest.java index 681d1c7..95a3b04 100644 --- a/gbfs-validator-java-loader/src/test/java/org/entur/gbfs/validator/loader/LoaderTest.java +++ b/gbfs-validator-java-loader/src/test/java/org/entur/gbfs/validator/loader/LoaderTest.java @@ -1,7 +1,16 @@ package org.entur.gbfs.validator.loader; +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; +import static org.junit.jupiter.api.Assertions.*; + import com.github.tomakehurst.wiremock.WireMockServer; import com.github.tomakehurst.wiremock.client.WireMock; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.List; import org.apache.hc.core5.http.HttpHeaders; import org.entur.gbfs.validator.loader.auth.BasicAuth; import org.entur.gbfs.validator.loader.auth.BearerTokenAuth; @@ -12,257 +21,377 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.util.Base64; -import java.util.List; - -import static com.github.tomakehurst.wiremock.client.WireMock.*; -import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; -import static org.junit.jupiter.api.Assertions.*; - @ExtendWith(MockitoExtension.class) public class LoaderTest { - private WireMockServer wireMockServer; - private Loader loader; - - private String gbfsDiscoveryJson = "{\"version\": \"3.0\", \"data\": {\"feeds\": []}}"; - private String systemInformationJson = "{\"system_id\": \"test-system\", \"language\": \"en\", \"name\": \"Test System\"}"; - - - @BeforeEach - void setUp() { - wireMockServer = new WireMockServer(wireMockConfig().dynamicPort()); - wireMockServer.start(); - WireMock.configureFor("localhost", wireMockServer.port()); - loader = new Loader(); - } - - @AfterEach - void tearDown() throws IOException { - wireMockServer.stop(); - loader.close(); - } - - private String getBaseUrl() { - return "http://localhost:" + wireMockServer.port(); - } - - private String convertStreamToString(InputStream is) throws IOException { - if (is == null) { - return null; - } - try (java.util.Scanner s = new java.util.Scanner(is).useDelimiter("\\A")) { - return s.hasNext() ? s.next() : ""; - } + private WireMockServer wireMockServer; + private Loader loader; + + private String gbfsDiscoveryJson = + "{\"version\": \"3.0\", \"data\": {\"feeds\": []}}"; + private String systemInformationJson = + "{\"system_id\": \"test-system\", \"language\": \"en\", \"name\": \"Test System\"}"; + + @BeforeEach + void setUp() { + wireMockServer = new WireMockServer(wireMockConfig().dynamicPort()); + wireMockServer.start(); + WireMock.configureFor("localhost", wireMockServer.port()); + loader = new Loader(); + } + + @AfterEach + void tearDown() throws IOException { + wireMockServer.stop(); + loader.close(); + } + + private String getBaseUrl() { + return "http://localhost:" + wireMockServer.port(); + } + + private String convertStreamToString(InputStream is) throws IOException { + if (is == null) { + return null; } - - @Test - void testLoadFile_NoAuth_Success() throws IOException { - stubFor(get(urlEqualTo("/gbfs.json")) - .willReturn(aResponse() - .withHeader("Content-Type", "application/json") - .withBody(gbfsDiscoveryJson))); - - List files = loader.load(getBaseUrl() + "/gbfs.json"); - - assertNotNull(files); - assertEquals(1, files.size()); - LoadedFile gbfsFile = files.get(0); - assertNotNull(gbfsFile.fileContents()); - assertTrue(gbfsFile.loaderErrors().isEmpty(), "Expected no system errors"); - assertEquals(gbfsDiscoveryJson, convertStreamToString(gbfsFile.fileContents())); - - wireMockServer.verify(getRequestedFor(urlEqualTo("/gbfs.json")) - .withoutHeader(HttpHeaders.AUTHORIZATION)); - } - - @Test - void testLoadFile_BasicAuth_Success() throws IOException { - String username = "testuser"; - String password = "testpassword"; - String expectedAuthHeader = "Basic " + Base64.getEncoder().encodeToString((username + ":" + password).getBytes(StandardCharsets.UTF_8)); - - stubFor(get(urlEqualTo("/gbfs.json")) - .withHeader(HttpHeaders.AUTHORIZATION, equalTo(expectedAuthHeader)) - .willReturn(aResponse() - .withHeader("Content-Type", "application/json") - .withBody(gbfsDiscoveryJson))); - - BasicAuth basicAuth = new BasicAuth(username, password); - List files = loader.load(getBaseUrl() + "/gbfs.json", basicAuth); - - assertNotNull(files); - assertEquals(1, files.size()); - LoadedFile gbfsFile = files.get(0); - assertNotNull(gbfsFile.fileContents()); - assertTrue(gbfsFile.loaderErrors().isEmpty(), "Expected no system errors"); - assertEquals(gbfsDiscoveryJson, convertStreamToString(gbfsFile.fileContents())); - - wireMockServer.verify(getRequestedFor(urlEqualTo("/gbfs.json")) - .withHeader(HttpHeaders.AUTHORIZATION, equalTo(expectedAuthHeader))); - } - - @Test - void testLoadFile_BasicAuth_Failure_WrongCredentials() throws IOException { - stubFor(get(urlEqualTo("/gbfs.json")) - .willReturn(aResponse().withStatus(401))); // Mock server returns 401 for any auth - - BasicAuth basicAuth = new BasicAuth("wronguser", "wrongpassword"); - List files = loader.load(getBaseUrl() + "/gbfs.json", basicAuth); - - assertNotNull(files); - assertEquals(1, files.size()); - LoadedFile gbfsFile = files.get(0); - assertNull(gbfsFile.fileContents()); - assertFalse(gbfsFile.loaderErrors().isEmpty(), "Expected system errors"); - assertEquals("CONNECTION_ERROR", gbfsFile.loaderErrors().get(0).error()); - assertTrue(gbfsFile.loaderErrors().get(0).message().contains("401")); - } - - @Test - void testLoadFile_BearerTokenAuth_Success() throws IOException { - String token = "test_token"; - String expectedAuthHeader = "Bearer " + token; - - stubFor(get(urlEqualTo("/gbfs.json")) - .withHeader(HttpHeaders.AUTHORIZATION, equalTo(expectedAuthHeader)) - .willReturn(aResponse() - .withHeader("Content-Type", "application/json") - .withBody(gbfsDiscoveryJson))); - - BearerTokenAuth bearerAuth = new BearerTokenAuth(token); - List files = loader.load(getBaseUrl() + "/gbfs.json", bearerAuth); - - assertNotNull(files); - assertEquals(1, files.size()); - LoadedFile gbfsFile = files.get(0); - assertNotNull(gbfsFile.fileContents()); - assertTrue(gbfsFile.loaderErrors().isEmpty(), "Expected no system errors"); - assertEquals(gbfsDiscoveryJson, convertStreamToString(gbfsFile.fileContents())); - - wireMockServer.verify(getRequestedFor(urlEqualTo("/gbfs.json")) - .withHeader(HttpHeaders.AUTHORIZATION, equalTo(expectedAuthHeader))); - } - - @Test - void testLoadFile_OAuthClientCredentials_Success() throws IOException { - String clientId = "testClient"; - String clientSecret = "testSecret"; - String token = "oauth_test_token"; - String tokenUrl = "/oauth/token"; - String gbfsUrl = "/gbfs.json"; - - // 1. Mock OAuth token endpoint - stubFor(post(urlEqualTo(tokenUrl)) - .withHeader("Content-Type", equalTo("application/x-www-form-urlencoded")) - .withRequestBody(containing("grant_type=client_credentials")) - .withRequestBody(containing("client_id=" + clientId)) - .withRequestBody(containing("client_secret=" + clientSecret)) - .willReturn(aResponse() - .withHeader("Content-Type", "application/json") - .withBody("{\"access_token\": \"" + token + "\", \"token_type\": \"Bearer\"}"))); - - // 2. Mock GBFS file endpoint - stubFor(get(urlEqualTo(gbfsUrl)) - .withHeader(HttpHeaders.AUTHORIZATION, equalTo("Bearer " + token)) - .willReturn(aResponse() - .withHeader("Content-Type", "application/json") - .withBody(gbfsDiscoveryJson))); - - OAuthClientCredentialsGrantAuth oauthAuth = new OAuthClientCredentialsGrantAuth(clientId, clientSecret, getBaseUrl() + tokenUrl); - List files = loader.load(getBaseUrl() + gbfsUrl, oauthAuth); - - assertNotNull(files); - assertEquals(1, files.size()); - LoadedFile gbfsFile = files.get(0); - assertNotNull(gbfsFile.fileContents(), "File contents should not be null on success"); - assertTrue(gbfsFile.loaderErrors().isEmpty(), "Expected no system errors. Errors: " + gbfsFile.loaderErrors()); - assertEquals(gbfsDiscoveryJson, convertStreamToString(gbfsFile.fileContents())); - - wireMockServer.verify(postRequestedFor(urlEqualTo(tokenUrl))); - wireMockServer.verify(getRequestedFor(urlEqualTo(gbfsUrl)) - .withHeader(HttpHeaders.AUTHORIZATION, equalTo("Bearer " + token))); - } - - @Test - void testLoadFile_OAuthClientCredentials_TokenFetchFailure() throws IOException { - String clientId = "testClient"; - String clientSecret = "testSecret"; - String tokenUrl = "/oauth/token"; - String gbfsUrl = "/gbfs.json"; - - // Mock OAuth token endpoint to return an error - stubFor(post(urlEqualTo(tokenUrl)) - .willReturn(aResponse().withStatus(500).withBody("OAuth server error"))); - - OAuthClientCredentialsGrantAuth oauthAuth = new OAuthClientCredentialsGrantAuth(clientId, clientSecret, getBaseUrl() + tokenUrl); - List files = loader.load(getBaseUrl() + gbfsUrl, oauthAuth); - - assertNotNull(files); - assertEquals(1, files.size()); - LoadedFile gbfsFile = files.get(0); - assertNull(gbfsFile.fileContents()); - assertFalse(gbfsFile.loaderErrors().isEmpty(), "Expected system errors due to token fetch failure"); - LoaderError error = gbfsFile.loaderErrors().get(0); - assertEquals("CONNECTION_ERROR", error.error()); // Loader wraps it in CONNECTION_ERROR - assertTrue(error.message().contains("OAuth token fetch failed"), "Error message should indicate token fetch failure. Was: " + error.message()); - - wireMockServer.verify(postRequestedFor(urlEqualTo(tokenUrl))); - wireMockServer.verify(0, getRequestedFor(urlEqualTo(gbfsUrl))); // GBFS endpoint should not be called - } - - - @Test - void testLoad_WithDiscoveryFileAndFeed_V3_WithAuth() throws IOException { - String token = "test_token_v3"; - String expectedAuthHeader = "Bearer " + token; - String discoveryUrl = "/gbfs-v3.json"; - String systemInfoUrl = "/system_information-v3.json"; - - String discoveryContentWithFeed = String.format( - "{\"version\": \"3.0\", \"data\": {\"feeds\": [{\"name\": \"system_information\", \"url\": \"%s%s\"}]}}", - getBaseUrl(), systemInfoUrl); - - // Mock discovery file - stubFor(get(urlEqualTo(discoveryUrl)) - .withHeader(HttpHeaders.AUTHORIZATION, equalTo(expectedAuthHeader)) - .willReturn(aResponse() - .withHeader("Content-Type", "application/json") - .withBody(discoveryContentWithFeed))); - - // Mock system_information file - stubFor(get(urlEqualTo(systemInfoUrl)) - .withHeader(HttpHeaders.AUTHORIZATION, equalTo(expectedAuthHeader)) - .willReturn(aResponse() - .withHeader("Content-Type", "application/json") - .withBody(systemInformationJson))); - - BearerTokenAuth bearerAuth = new BearerTokenAuth(token); - List files = loader.load(getBaseUrl() + discoveryUrl, bearerAuth); - - assertNotNull(files); - assertEquals(2, files.size(), "Expected discovery file and one feed file"); - - LoadedFile discoveryFile = files.stream().filter(f -> f.fileName().equals("gbfs-v3.json")).findFirst().orElse(null); - LoadedFile systemInfoFile = files.stream().filter(f -> f.fileName().equals("system_information")).findFirst().orElse(null); - - assertNotNull(discoveryFile, "Discovery file should be loaded"); - assertNotNull(discoveryFile.fileContents()); - assertTrue(discoveryFile.loaderErrors().isEmpty(), "Discovery file should have no errors"); - assertEquals(discoveryContentWithFeed, convertStreamToString(discoveryFile.fileContents())); - - - assertNotNull(systemInfoFile, "System Information file should be loaded"); - assertNotNull(systemInfoFile.fileContents()); - assertTrue(systemInfoFile.loaderErrors().isEmpty(), "System Information file should have no errors. Errors: " + (systemInfoFile.loaderErrors().isEmpty() ? "None" : systemInfoFile.loaderErrors().get(0).toString())); - assertEquals(systemInformationJson, convertStreamToString(systemInfoFile.fileContents())); - - wireMockServer.verify(getRequestedFor(urlEqualTo(discoveryUrl)) - .withHeader(HttpHeaders.AUTHORIZATION, equalTo(expectedAuthHeader))); - wireMockServer.verify(getRequestedFor(urlEqualTo(systemInfoUrl)) - .withHeader(HttpHeaders.AUTHORIZATION, equalTo(expectedAuthHeader))); + try (java.util.Scanner s = new java.util.Scanner(is).useDelimiter("\\A")) { + return s.hasNext() ? s.next() : ""; } + } + + @Test + void testLoadFile_NoAuth_Success() throws IOException { + stubFor( + get(urlEqualTo("/gbfs.json")) + .willReturn( + aResponse() + .withHeader("Content-Type", "application/json") + .withBody(gbfsDiscoveryJson) + ) + ); + + List files = loader.load(getBaseUrl() + "/gbfs.json"); + + assertNotNull(files); + assertEquals(1, files.size()); + LoadedFile gbfsFile = files.get(0); + assertNotNull(gbfsFile.fileContents()); + assertTrue(gbfsFile.loaderErrors().isEmpty(), "Expected no system errors"); + assertEquals( + gbfsDiscoveryJson, + convertStreamToString(gbfsFile.fileContents()) + ); + + wireMockServer.verify( + getRequestedFor(urlEqualTo("/gbfs.json")) + .withoutHeader(HttpHeaders.AUTHORIZATION) + ); + } + + @Test + void testLoadFile_BasicAuth_Success() throws IOException { + String username = "testuser"; + String password = "testpassword"; + String expectedAuthHeader = + "Basic " + + Base64 + .getEncoder() + .encodeToString( + (username + ":" + password).getBytes(StandardCharsets.UTF_8) + ); + + stubFor( + get(urlEqualTo("/gbfs.json")) + .withHeader(HttpHeaders.AUTHORIZATION, equalTo(expectedAuthHeader)) + .willReturn( + aResponse() + .withHeader("Content-Type", "application/json") + .withBody(gbfsDiscoveryJson) + ) + ); + + BasicAuth basicAuth = new BasicAuth(username, password); + List files = loader.load( + getBaseUrl() + "/gbfs.json", + basicAuth + ); + + assertNotNull(files); + assertEquals(1, files.size()); + LoadedFile gbfsFile = files.get(0); + assertNotNull(gbfsFile.fileContents()); + assertTrue(gbfsFile.loaderErrors().isEmpty(), "Expected no system errors"); + assertEquals( + gbfsDiscoveryJson, + convertStreamToString(gbfsFile.fileContents()) + ); + + wireMockServer.verify( + getRequestedFor(urlEqualTo("/gbfs.json")) + .withHeader(HttpHeaders.AUTHORIZATION, equalTo(expectedAuthHeader)) + ); + } + + @Test + void testLoadFile_BasicAuth_Failure_WrongCredentials() throws IOException { + stubFor( + get(urlEqualTo("/gbfs.json")).willReturn(aResponse().withStatus(401)) + ); // Mock server returns 401 for any auth + + BasicAuth basicAuth = new BasicAuth("wronguser", "wrongpassword"); + List files = loader.load( + getBaseUrl() + "/gbfs.json", + basicAuth + ); + + assertNotNull(files); + assertEquals(1, files.size()); + LoadedFile gbfsFile = files.get(0); + assertNull(gbfsFile.fileContents()); + assertFalse(gbfsFile.loaderErrors().isEmpty(), "Expected system errors"); + assertEquals("CONNECTION_ERROR", gbfsFile.loaderErrors().get(0).error()); + assertTrue(gbfsFile.loaderErrors().get(0).message().contains("401")); + } + + @Test + void testLoadFile_BearerTokenAuth_Success() throws IOException { + String token = "test_token"; + String expectedAuthHeader = "Bearer " + token; + + stubFor( + get(urlEqualTo("/gbfs.json")) + .withHeader(HttpHeaders.AUTHORIZATION, equalTo(expectedAuthHeader)) + .willReturn( + aResponse() + .withHeader("Content-Type", "application/json") + .withBody(gbfsDiscoveryJson) + ) + ); + + BearerTokenAuth bearerAuth = new BearerTokenAuth(token); + List files = loader.load( + getBaseUrl() + "/gbfs.json", + bearerAuth + ); + + assertNotNull(files); + assertEquals(1, files.size()); + LoadedFile gbfsFile = files.get(0); + assertNotNull(gbfsFile.fileContents()); + assertTrue(gbfsFile.loaderErrors().isEmpty(), "Expected no system errors"); + assertEquals( + gbfsDiscoveryJson, + convertStreamToString(gbfsFile.fileContents()) + ); + + wireMockServer.verify( + getRequestedFor(urlEqualTo("/gbfs.json")) + .withHeader(HttpHeaders.AUTHORIZATION, equalTo(expectedAuthHeader)) + ); + } + + @Test + void testLoadFile_OAuthClientCredentials_Success() throws IOException { + String clientId = "testClient"; + String clientSecret = "testSecret"; + String token = "oauth_test_token"; + String tokenUrl = "/oauth/token"; + String gbfsUrl = "/gbfs.json"; + + // 1. Mock OAuth token endpoint + stubFor( + post(urlEqualTo(tokenUrl)) + .withHeader( + "Content-Type", + equalTo("application/x-www-form-urlencoded") + ) + .withRequestBody(containing("grant_type=client_credentials")) + .withRequestBody(containing("client_id=" + clientId)) + .withRequestBody(containing("client_secret=" + clientSecret)) + .willReturn( + aResponse() + .withHeader("Content-Type", "application/json") + .withBody( + "{\"access_token\": \"" + + token + + "\", \"token_type\": \"Bearer\"}" + ) + ) + ); + + // 2. Mock GBFS file endpoint + stubFor( + get(urlEqualTo(gbfsUrl)) + .withHeader(HttpHeaders.AUTHORIZATION, equalTo("Bearer " + token)) + .willReturn( + aResponse() + .withHeader("Content-Type", "application/json") + .withBody(gbfsDiscoveryJson) + ) + ); + + OAuthClientCredentialsGrantAuth oauthAuth = + new OAuthClientCredentialsGrantAuth( + clientId, + clientSecret, + getBaseUrl() + tokenUrl + ); + List files = loader.load(getBaseUrl() + gbfsUrl, oauthAuth); + + assertNotNull(files); + assertEquals(1, files.size()); + LoadedFile gbfsFile = files.get(0); + assertNotNull( + gbfsFile.fileContents(), + "File contents should not be null on success" + ); + assertTrue( + gbfsFile.loaderErrors().isEmpty(), + "Expected no system errors. Errors: " + gbfsFile.loaderErrors() + ); + assertEquals( + gbfsDiscoveryJson, + convertStreamToString(gbfsFile.fileContents()) + ); + + wireMockServer.verify(postRequestedFor(urlEqualTo(tokenUrl))); + wireMockServer.verify( + getRequestedFor(urlEqualTo(gbfsUrl)) + .withHeader(HttpHeaders.AUTHORIZATION, equalTo("Bearer " + token)) + ); + } + + @Test + void testLoadFile_OAuthClientCredentials_TokenFetchFailure() + throws IOException { + String clientId = "testClient"; + String clientSecret = "testSecret"; + String tokenUrl = "/oauth/token"; + String gbfsUrl = "/gbfs.json"; + + // Mock OAuth token endpoint to return an error + stubFor( + post(urlEqualTo(tokenUrl)) + .willReturn(aResponse().withStatus(500).withBody("OAuth server error")) + ); + + OAuthClientCredentialsGrantAuth oauthAuth = + new OAuthClientCredentialsGrantAuth( + clientId, + clientSecret, + getBaseUrl() + tokenUrl + ); + List files = loader.load(getBaseUrl() + gbfsUrl, oauthAuth); + + assertNotNull(files); + assertEquals(1, files.size()); + LoadedFile gbfsFile = files.get(0); + assertNull(gbfsFile.fileContents()); + assertFalse( + gbfsFile.loaderErrors().isEmpty(), + "Expected system errors due to token fetch failure" + ); + LoaderError error = gbfsFile.loaderErrors().get(0); + assertEquals("CONNECTION_ERROR", error.error()); // Loader wraps it in CONNECTION_ERROR + assertTrue( + error.message().contains("OAuth token fetch failed"), + "Error message should indicate token fetch failure. Was: " + + error.message() + ); + + wireMockServer.verify(postRequestedFor(urlEqualTo(tokenUrl))); + wireMockServer.verify(0, getRequestedFor(urlEqualTo(gbfsUrl))); // GBFS endpoint should not be called + } + + @Test + void testLoad_WithDiscoveryFileAndFeed_V3_WithAuth() throws IOException { + String token = "test_token_v3"; + String expectedAuthHeader = "Bearer " + token; + String discoveryUrl = "/gbfs-v3.json"; + String systemInfoUrl = "/system_information-v3.json"; + + String discoveryContentWithFeed = String.format( + "{\"version\": \"3.0\", \"data\": {\"feeds\": [{\"name\": \"system_information\", \"url\": \"%s%s\"}]}}", + getBaseUrl(), + systemInfoUrl + ); + + // Mock discovery file + stubFor( + get(urlEqualTo(discoveryUrl)) + .withHeader(HttpHeaders.AUTHORIZATION, equalTo(expectedAuthHeader)) + .willReturn( + aResponse() + .withHeader("Content-Type", "application/json") + .withBody(discoveryContentWithFeed) + ) + ); + + // Mock system_information file + stubFor( + get(urlEqualTo(systemInfoUrl)) + .withHeader(HttpHeaders.AUTHORIZATION, equalTo(expectedAuthHeader)) + .willReturn( + aResponse() + .withHeader("Content-Type", "application/json") + .withBody(systemInformationJson) + ) + ); + + BearerTokenAuth bearerAuth = new BearerTokenAuth(token); + List files = loader.load( + getBaseUrl() + discoveryUrl, + bearerAuth + ); + + assertNotNull(files); + assertEquals(2, files.size(), "Expected discovery file and one feed file"); + + LoadedFile discoveryFile = files + .stream() + .filter(f -> f.fileName().equals("gbfs-v3.json")) + .findFirst() + .orElse(null); + LoadedFile systemInfoFile = files + .stream() + .filter(f -> f.fileName().equals("system_information")) + .findFirst() + .orElse(null); + + assertNotNull(discoveryFile, "Discovery file should be loaded"); + assertNotNull(discoveryFile.fileContents()); + assertTrue( + discoveryFile.loaderErrors().isEmpty(), + "Discovery file should have no errors" + ); + assertEquals( + discoveryContentWithFeed, + convertStreamToString(discoveryFile.fileContents()) + ); + + assertNotNull(systemInfoFile, "System Information file should be loaded"); + assertNotNull(systemInfoFile.fileContents()); + assertTrue( + systemInfoFile.loaderErrors().isEmpty(), + "System Information file should have no errors. Errors: " + + ( + systemInfoFile.loaderErrors().isEmpty() + ? "None" + : systemInfoFile.loaderErrors().get(0).toString() + ) + ); + assertEquals( + systemInformationJson, + convertStreamToString(systemInfoFile.fileContents()) + ); + + wireMockServer.verify( + getRequestedFor(urlEqualTo(discoveryUrl)) + .withHeader(HttpHeaders.AUTHORIZATION, equalTo(expectedAuthHeader)) + ); + wireMockServer.verify( + getRequestedFor(urlEqualTo(systemInfoUrl)) + .withHeader(HttpHeaders.AUTHORIZATION, equalTo(expectedAuthHeader)) + ); + } } diff --git a/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/GbfsValidator.java b/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/GbfsValidator.java index 20ce91d..52e9a9e 100644 --- a/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/GbfsValidator.java +++ b/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/GbfsValidator.java @@ -18,31 +18,29 @@ package org.entur.gbfs.validation; -import org.entur.gbfs.validation.model.FileValidationResult; -import org.entur.gbfs.validation.model.ValidationResult; - import java.io.InputStream; import java.util.Map; +import org.entur.gbfs.validation.model.FileValidationResult; +import org.entur.gbfs.validation.model.ValidationResult; /** * Represents a validator of GBFS files */ public interface GbfsValidator { + /** + * Validate all files in the map of GBFS files, keyed by the name of the file. Will validate using + * custom rules in addition to the static schema + * @param fileMap + * @return + */ + ValidationResult validate(Map fileMap); - /** - * Validate all files in the map of GBFS files, keyed by the name of the file. Will validate using - * custom rules in addition to the static schema - * @param fileMap - * @return - */ - ValidationResult validate(Map fileMap); - - /** - * Validate the GBFS file with the given name and the file itself as an InputStream. Will not apply - * custom rules, but only validate using the static schema - * @param fileName - * @param file - * @return - */ - FileValidationResult validateFile(String fileName, InputStream file); + /** + * Validate the GBFS file with the given name and the file itself as an InputStream. Will not apply + * custom rules, but only validate using the static schema + * @param fileName + * @param file + * @return + */ + FileValidationResult validateFile(String fileName, InputStream file); } diff --git a/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/GbfsValidatorFactory.java b/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/GbfsValidatorFactory.java index 3aa7e06..a9a7b1f 100644 --- a/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/GbfsValidatorFactory.java +++ b/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/GbfsValidatorFactory.java @@ -24,12 +24,13 @@ * Main library entrypoint */ public class GbfsValidatorFactory { - private GbfsValidatorFactory() {} - /** - * Get a GbfsValidator instance - */ - public static GbfsValidator getGbfsJsonValidator() { - return new GbfsJsonValidator(); - } + private GbfsValidatorFactory() {} + + /** + * Get a GbfsValidator instance + */ + public static GbfsValidator getGbfsJsonValidator() { + return new GbfsJsonValidator(); + } } diff --git a/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/model/FileValidationError.java b/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/model/FileValidationError.java index c678211..fa32879 100644 --- a/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/model/FileValidationError.java +++ b/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/model/FileValidationError.java @@ -27,19 +27,18 @@ * @param message An error message */ public record FileValidationError( - String schemaPath, - String violationPath, - String message, - String keyword -) implements ValidationResultComponentIdentity { - - @Override - public boolean sameAs(FileValidationError other) { - if (other == null) return false; - if (!Objects.equals(schemaPath, other.schemaPath)) return false; - if (!Objects.equals(violationPath, other.violationPath)) - return false; - if (!Objects.equals(message, other.message)) return false; - return Objects.equals(keyword, other.keyword); - } + String schemaPath, + String violationPath, + String message, + String keyword +) + implements ValidationResultComponentIdentity { + @Override + public boolean sameAs(FileValidationError other) { + if (other == null) return false; + if (!Objects.equals(schemaPath, other.schemaPath)) return false; + if (!Objects.equals(violationPath, other.violationPath)) return false; + if (!Objects.equals(message, other.message)) return false; + return Objects.equals(keyword, other.keyword); + } } diff --git a/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/model/FileValidationResult.java b/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/model/FileValidationResult.java index 19d07bb..c148e3a 100644 --- a/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/model/FileValidationResult.java +++ b/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/model/FileValidationResult.java @@ -36,55 +36,72 @@ * @param validatorErrors A list of system errors encountered while trying to load or process the file */ public record FileValidationResult( - String file, - boolean required, - boolean exists, - int errorsCount, - String schema, - String fileContents, - String version, - List errors, - List validatorErrors -) implements ValidationResultComponentIdentity { + String file, + boolean required, + boolean exists, + int errorsCount, + String schema, + String fileContents, + String version, + List errors, + List validatorErrors +) + implements ValidationResultComponentIdentity { + public FileValidationResult { + errors = new ArrayList<>(errors); + validatorErrors = new ArrayList<>(validatorErrors); + } - public FileValidationResult { - errors = new ArrayList<>(errors); - validatorErrors = new ArrayList<>(validatorErrors); - } - - @Override - public String toString() { - return "FileValidationResult{" + - "file='" + file + '\'' + - ", required=" + required + - ", exists=" + exists + - ", errorsCount=" + errorsCount + - ", schema='" + schema + '\'' + - ", fileContents='" + fileContents + '\'' + - ", version='" + version + '\'' + - ", errors=" + errors + - ", systemErrors=" + validatorErrors + - '}'; - } + @Override + public String toString() { + return ( + "FileValidationResult{" + + "file='" + + file + + '\'' + + ", required=" + + required + + ", exists=" + + exists + + ", errorsCount=" + + errorsCount + + ", schema='" + + schema + + '\'' + + ", fileContents='" + + fileContents + + '\'' + + ", version='" + + version + + '\'' + + ", errors=" + + errors + + ", systemErrors=" + + validatorErrors + + '}' + ); + } - @Override - public boolean sameAs(FileValidationResult other) { - if (other == null) return false; - if (required != other.required) 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; - if (!Objects.equals(version, other.version)) return false; + @Override + public boolean sameAs(FileValidationResult other) { + if (other == null) return false; + if (required != other.required) 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; + if (!Objects.equals(version, other.version)) return false; - // Compare validation errors - if (errors.size() != other.errors.size()) return false; - if (!IntStream - .range(0, errors.size()) - .allMatch(i -> errors.get(i).sameAs(other.errors.get(i)))) { - return false; - } - - // Compare system errors (SystemError is a record, so its equals method is suitable) - return Objects.equals(validatorErrors, other.validatorErrors); + // Compare validation errors + if (errors.size() != other.errors.size()) return false; + if ( + !IntStream + .range(0, errors.size()) + .allMatch(i -> errors.get(i).sameAs(other.errors.get(i))) + ) { + return false; } + + // Compare system errors (SystemError is a record, so its equals method is suitable) + return Objects.equals(validatorErrors, other.validatorErrors); + } } diff --git a/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/model/ValidationResult.java b/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/model/ValidationResult.java index dc8107d..745b741 100644 --- a/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/model/ValidationResult.java +++ b/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/model/ValidationResult.java @@ -26,21 +26,26 @@ * @param files A map of files that were validated */ public record ValidationResult( - ValidationSummary summary, - Map files -) implements ValidationResultComponentIdentity { - @Override - public String toString() { - return "ValidationResult{" + - "summary=" + summary + - ", files=" + files + - '}'; - } + ValidationSummary summary, + Map files +) + implements ValidationResultComponentIdentity { + @Override + public String toString() { + return ( + "ValidationResult{" + "summary=" + summary + ", files=" + files + '}' + ); + } - @Override - public boolean sameAs(ValidationResult other) { - if (other == null) return false; - if (!summary.sameAs(other.summary())) return false; - return files.entrySet().stream().allMatch(entry -> other.files.get(entry.getKey()).sameAs(entry.getValue())); - } + @Override + public boolean sameAs(ValidationResult other) { + if (other == null) return false; + if (!summary.sameAs(other.summary())) return false; + return files + .entrySet() + .stream() + .allMatch(entry -> + other.files.get(entry.getKey()).sameAs(entry.getValue()) + ); + } } diff --git a/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/model/ValidationResultComponentIdentity.java b/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/model/ValidationResultComponentIdentity.java index b2e6622..1118fe2 100644 --- a/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/model/ValidationResultComponentIdentity.java +++ b/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/model/ValidationResultComponentIdentity.java @@ -21,10 +21,9 @@ package org.entur.gbfs.validation.model; public interface ValidationResultComponentIdentity { - - /** - * Check if the (part of) a validation result is the same as another one. Should return true for two validation - * events at different times with the same outcome - */ - boolean sameAs(T other); + /** + * Check if the (part of) a validation result is the same as another one. Should return true for two validation + * events at different times with the same outcome + */ + boolean sameAs(T other); } diff --git a/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/model/ValidationSummary.java b/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/model/ValidationSummary.java index e06a99f..71e1b6d 100644 --- a/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/model/ValidationSummary.java +++ b/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/model/ValidationSummary.java @@ -26,25 +26,27 @@ * @param timestamp The time when validation was performed * @param errorsCount The total amount of errors encountered during validation */ -public record ValidationSummary( - String version, - long timestamp, - int errorsCount -) implements ValidationResultComponentIdentity { +public record ValidationSummary(String version, long timestamp, int errorsCount) + implements ValidationResultComponentIdentity { + @Override + public String toString() { + return ( + "ValidationSummary{" + + "version='" + + version + + '\'' + + ", timestamp=" + + timestamp + + ", errorsCount=" + + errorsCount + + '}' + ); + } - @Override - public String toString() { - return "ValidationSummary{" + - "version='" + version + '\'' + - ", timestamp=" + timestamp + - ", errorsCount=" + errorsCount + - '}'; - } - - @Override - public boolean sameAs(ValidationSummary other) { - if (other == null) return false; - if (errorsCount != other.errorsCount) return false; - return Objects.equals(version, other.version); - } + @Override + public boolean sameAs(ValidationSummary other) { + if (other == null) return false; + if (errorsCount != other.errorsCount) return false; + return Objects.equals(version, other.version); + } } diff --git a/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/model/ValidatorError.java b/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/model/ValidatorError.java index 630c0ef..5cd865c 100644 --- a/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/model/ValidatorError.java +++ b/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/model/ValidatorError.java @@ -1,5 +1,5 @@ package org.entur.gbfs.validation.model; public record ValidatorError(String error, String message) { - // No additional body needed for a simple record + // No additional body needed for a simple record } diff --git a/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/validator/FileValidator.java b/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/validator/FileValidator.java index 90f2e0a..b302663 100644 --- a/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/validator/FileValidator.java +++ b/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/validator/FileValidator.java @@ -18,6 +18,10 @@ package org.entur.gbfs.validation.validator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; import org.entur.gbfs.validation.model.FileValidationError; import org.entur.gbfs.validation.model.FileValidationResult; import org.entur.gbfs.validation.validator.versions.Version; @@ -27,102 +31,106 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; - public class FileValidator { - private static final Logger logger = LoggerFactory.getLogger(FileValidator.class); - private final Version version; - private static final Map FILE_VALIDATORS = new ConcurrentHashMap<>(); - - - public static FileValidator getFileValidator( - String detectedVersion - ) { - if (FILE_VALIDATORS.containsKey(detectedVersion)) { - return FILE_VALIDATORS.get(detectedVersion); - } else { - Version version = VersionFactory.createVersion(detectedVersion); - FileValidator fileValidator = new FileValidator(version); - FILE_VALIDATORS.put(detectedVersion, fileValidator); - return fileValidator; - } + private static final Logger logger = LoggerFactory.getLogger( + FileValidator.class + ); + private final Version version; + private static final Map FILE_VALIDATORS = + new ConcurrentHashMap<>(); + + public static FileValidator getFileValidator(String detectedVersion) { + if (FILE_VALIDATORS.containsKey(detectedVersion)) { + return FILE_VALIDATORS.get(detectedVersion); + } else { + Version version = VersionFactory.createVersion(detectedVersion); + + FileValidator fileValidator = new FileValidator(version); + FILE_VALIDATORS.put(detectedVersion, fileValidator); + return fileValidator; } - - protected FileValidator( - Version version - ) { - this.version = version; - } - - public FileValidationResult validate(String feedName, Map feedMap) { - if (version.getFileNames().contains(feedName)) { - JSONObject feed = feedMap.get(feedName); - int errorsCount = 0; - List validationErrors = List.of(); - - try { - version.validate(feedName, feedMap); - } catch (ValidationException validationException) { - errorsCount = validationException.getViolationCount(); - validationErrors = mapToValidationErrors(validationException); - } - - return new FileValidationResult( - feedName, - isRequired(feedName), - feed != null, - errorsCount, - version.getSchema(feedName, feedMap).toString(), - Optional.ofNullable(feed).map(JSONObject::toString).orElse(null), - version.getVersionString(), - validationErrors, - java.util.Collections.emptyList() // Added for systemErrors - ); - } - - logger.warn("Schema not found for gbfs feed={} version={}", feedName, version.getVersionString()); - return null; + } + + protected FileValidator(Version version) { + this.version = version; + } + + public FileValidationResult validate( + String feedName, + Map feedMap + ) { + if (version.getFileNames().contains(feedName)) { + JSONObject feed = feedMap.get(feedName); + int errorsCount = 0; + List validationErrors = List.of(); + + try { + version.validate(feedName, feedMap); + } catch (ValidationException validationException) { + errorsCount = validationException.getViolationCount(); + validationErrors = mapToValidationErrors(validationException); + } + + return new FileValidationResult( + feedName, + isRequired(feedName), + feed != null, + errorsCount, + version.getSchema(feedName, feedMap).toString(), + Optional.ofNullable(feed).map(JSONObject::toString).orElse(null), + version.getVersionString(), + validationErrors, + java.util.Collections.emptyList() // Added for systemErrors + ); } - List mapToValidationErrors(ValidationException validationException) { - if (validationException.getCausingExceptions().isEmpty()) { - return List.of( - new FileValidationError( - validationException.getSchemaLocation(), - validationException.getPointerToViolation(), - validationException.getMessage(), - validationException.getKeyword() - ) - ); - } else { - return validationException.getCausingExceptions().stream() - .map(this::mapToValidationErrors) - .flatMap(List::stream) - .toList(); - } - } - - private boolean isRequired(String feedName) { - return version.isFileRequired(feedName); - } - - - public FileValidationResult validateMissingFile(String file) { - var isRequired = version.isFileRequired(file); - return new FileValidationResult( - file, - isRequired, - false, - isRequired ? 1 : 0, - version.getSchema(file).toString(), - null, - version.getVersionString(), - List.of(), - java.util.Collections.emptyList() - ); + logger.warn( + "Schema not found for gbfs feed={} version={}", + feedName, + version.getVersionString() + ); + return null; + } + + List mapToValidationErrors( + ValidationException validationException + ) { + if (validationException.getCausingExceptions().isEmpty()) { + return List.of( + new FileValidationError( + validationException.getSchemaLocation(), + validationException.getPointerToViolation(), + validationException.getMessage(), + validationException.getKeyword() + ) + ); + } else { + return validationException + .getCausingExceptions() + .stream() + .map(this::mapToValidationErrors) + .flatMap(List::stream) + .toList(); } + } + + private boolean isRequired(String feedName) { + return version.isFileRequired(feedName); + } + + public FileValidationResult validateMissingFile(String file) { + var isRequired = version.isFileRequired(file); + return new FileValidationResult( + file, + isRequired, + false, + isRequired ? 1 : 0, + version.getSchema(file).toString(), + null, + version.getVersionString(), + List.of(), + java.util.Collections.emptyList() + ); + } } diff --git a/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/validator/GbfsJsonValidator.java b/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/validator/GbfsJsonValidator.java index 39bf165..3683564 100644 --- a/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/validator/GbfsJsonValidator.java +++ b/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/validator/GbfsJsonValidator.java @@ -18,18 +18,6 @@ package org.entur.gbfs.validation.validator; -import org.entur.gbfs.validation.GbfsValidator; -import org.entur.gbfs.validation.model.FileValidationResult; -import org.entur.gbfs.validation.model.ValidationResult; -import org.entur.gbfs.validation.model.ValidationSummary; -import org.entur.gbfs.validation.model.ValidatorError; // Changed to use model.SystemError -import org.entur.gbfs.validation.validator.versions.Version; -import org.entur.gbfs.validation.validator.versions.VersionFactory; -import org.json.JSONException; -import org.json.JSONObject; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; @@ -45,230 +33,324 @@ import java.util.Set; import java.util.function.Predicate; import java.util.stream.Collectors; +import org.entur.gbfs.validation.GbfsValidator; +import org.entur.gbfs.validation.model.FileValidationResult; +import org.entur.gbfs.validation.model.ValidationResult; +import org.entur.gbfs.validation.model.ValidationSummary; +import org.entur.gbfs.validation.model.ValidatorError; // Changed to use model.SystemError +import org.entur.gbfs.validation.validator.versions.Version; +import org.entur.gbfs.validation.validator.versions.VersionFactory; +import org.json.JSONException; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class GbfsJsonValidator implements GbfsValidator { - private static final Logger LOG = LoggerFactory.getLogger(GbfsJsonValidator.class); - - private static final String DEFAULT_VERSION = "2.3"; - private record ParsedFeedContainer( - String feedName, - JSONObject jsonObject, - List parsingErrors, - String originalContent + private static final Logger LOG = LoggerFactory.getLogger( + GbfsJsonValidator.class + ); + + private static final String DEFAULT_VERSION = "2.3"; + + private record ParsedFeedContainer( + String feedName, + JSONObject jsonObject, + List parsingErrors, + String originalContent + ) { + ParsedFeedContainer( + String feedName, + JSONObject jsonObject, + String originalContent ) { - ParsedFeedContainer(String feedName, JSONObject jsonObject, String originalContent) { - this(feedName, jsonObject, new ArrayList<>(), originalContent); - } + this(feedName, jsonObject, new ArrayList<>(), originalContent); } - - private static final List FEEDS = Arrays.asList( - "gbfs", - "gbfs_versions", - "system_information", - "vehicle_types", - "station_information", - "station_status", - "free_bike_status", - "vehicle_status", - "system_hours", - "system_alerts", - "system_alerts", - "system_calendar", - "system_regions", - "system_pricing_plans", - "geofencing_zones" - ); - - @Override - public ValidationResult validate(Map rawFeeds) { - Map parsedFeedsMap = parseFeeds(rawFeeds); - Map feedMap = parsedFeedsMap.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().jsonObject())); - Map fileValidations = new HashMap<>(); - - Version version = detectVersionFromParsedFeeds(parsedFeedsMap); - - for (String feedName : FEEDS) { - ParsedFeedContainer parsedContainer = parsedFeedsMap.get(feedName); - - if (parsedContainer == null) { - continue; - } - - if (parsedContainer.jsonObject() == null) { - // Parsing failed or stream read error - FileValidationResult result = new FileValidationResult( - feedName, - version.isFileRequired(feedName), - true, - 0, - version.getSchema(feedName).toString(), - parsedContainer.originalContent(), - null, - Collections.emptyList(), - parsedContainer.parsingErrors() - ); - fileValidations.put(feedName, result); - } else { - FileValidationResult validationResult = validateFile( - feedName, - feedMap - ); - if (validationResult != null) { - fileValidations.put(feedName, validationResult); - } - } - } - - // Re-evaluate version based on all successfully validated files, if necessary, or stick to initial. - // For now, the initial version detection is used for missing file checks. - version = findVersion(fileValidations); // This uses validated files' versions - - List missingFiles = findMissingFiles(version, fileValidations); - handleMissingFiles(fileValidations, missingFiles, version); // This creates FVRs for missing files - - ValidationSummary summary = new ValidationSummary( - version.getVersionString(), - System.currentTimeMillis(), - fileValidations.values().stream() - .filter(Objects::nonNull) - .map(FileValidationResult::errorsCount) // This counts only validation errors - .reduce(Integer::sum).orElse(0) + } + + private static final List FEEDS = Arrays.asList( + "gbfs", + "gbfs_versions", + "system_information", + "vehicle_types", + "station_information", + "station_status", + "free_bike_status", + "vehicle_status", + "system_hours", + "system_alerts", + "system_alerts", + "system_calendar", + "system_regions", + "system_pricing_plans", + "geofencing_zones" + ); + + @Override + public ValidationResult validate(Map rawFeeds) { + Map parsedFeedsMap = parseFeeds(rawFeeds); + Map feedMap = parsedFeedsMap + .entrySet() + .stream() + .collect( + Collectors.toMap( + Map.Entry::getKey, + entry -> entry.getValue().jsonObject() + ) + ); + Map fileValidations = new HashMap<>(); + + Version version = detectVersionFromParsedFeeds(parsedFeedsMap); + + for (String feedName : FEEDS) { + ParsedFeedContainer parsedContainer = parsedFeedsMap.get(feedName); + + if (parsedContainer == null) { + continue; + } + + if (parsedContainer.jsonObject() == null) { + // Parsing failed or stream read error + FileValidationResult result = new FileValidationResult( + feedName, + version.isFileRequired(feedName), + true, + 0, + version.getSchema(feedName).toString(), + parsedContainer.originalContent(), + null, + Collections.emptyList(), + parsedContainer.parsingErrors() ); - - return new ValidationResult(summary, fileValidations); - } - - - private Version detectVersionFromParsedFeeds(Map parsedFeeds) { - ParsedFeedContainer gbfsContainer = parsedFeeds.get("gbfs"); - if (gbfsContainer != null && gbfsContainer.jsonObject() != null) { - try { - String versionStr = gbfsContainer.jsonObject().getString("version"); - if (versionStr != null) { - return VersionFactory.createVersion(versionStr); - } - } catch (JSONException e) { - LOG.warn("Could not extract version from gbfs.json, using default.", e); - } + fileValidations.put(feedName, result); + } else { + FileValidationResult validationResult = validateFile(feedName, feedMap); + if (validationResult != null) { + fileValidations.put(feedName, validationResult); } - return VersionFactory.createVersion(GbfsJsonValidator.DEFAULT_VERSION); - } - - - private List findMissingFiles(Version version, Map fileValidations) { - return version.getFileNames().stream().filter(Predicate.not(fileValidations::containsKey)).toList(); + } } - @Override - public FileValidationResult validateFile(String fileName, InputStream file) { - ParsedFeedContainer parsedContainer = parseFeed(fileName, file); + // Re-evaluate version based on all successfully validated files, if necessary, or stick to initial. + // For now, the initial version detection is used for missing file checks. + version = findVersion(fileValidations); // This uses validated files' versions + + List missingFiles = findMissingFiles(version, fileValidations); + handleMissingFiles(fileValidations, missingFiles, version); // This creates FVRs for missing files + + ValidationSummary summary = new ValidationSummary( + version.getVersionString(), + System.currentTimeMillis(), + fileValidations + .values() + .stream() + .filter(Objects::nonNull) + .map(FileValidationResult::errorsCount) // This counts only validation errors + .reduce(Integer::sum) + .orElse(0) + ); - if (parsedContainer.jsonObject() == null) { - // Determine version for schema and requirement - this is tricky for a single file - // For now, using default version. A more robust approach might require context. - Version tempVersion = VersionFactory.createVersion(DEFAULT_VERSION); - return new FileValidationResult( - fileName, - tempVersion.isFileRequired(fileName), - true, // File was provided - 0, - tempVersion.getSchema(fileName).toString(), - parsedContainer.originalContent(), - null, // File specific version unknown - Collections.emptyList(), - parsedContainer.parsingErrors() - ); - } else { - return validateFile( - fileName, - Map.of(fileName, parsedContainer.jsonObject()) - ); + return new ValidationResult(summary, fileValidations); + } + + private Version detectVersionFromParsedFeeds( + Map parsedFeeds + ) { + ParsedFeedContainer gbfsContainer = parsedFeeds.get("gbfs"); + if (gbfsContainer != null && gbfsContainer.jsonObject() != null) { + try { + String versionStr = gbfsContainer.jsonObject().getString("version"); + if (versionStr != null) { + return VersionFactory.createVersion(versionStr); } + } catch (JSONException e) { + LOG.warn("Could not extract version from gbfs.json, using default.", e); + } } - - private void handleMissingFiles(Map fileValidations, List missingFiles, Version version) { - FileValidator fileValidator = FileValidator.getFileValidator(version.getVersionString()); - missingFiles - .forEach(file -> { - FileValidationResult missingResult = fileValidator.validateMissingFile(file); - fileValidations.put(file, missingResult); - } - ); + return VersionFactory.createVersion(GbfsJsonValidator.DEFAULT_VERSION); + } + + private List findMissingFiles( + Version version, + Map fileValidations + ) { + return version + .getFileNames() + .stream() + .filter(Predicate.not(fileValidations::containsKey)) + .toList(); + } + + @Override + public FileValidationResult validateFile(String fileName, InputStream file) { + ParsedFeedContainer parsedContainer = parseFeed(fileName, file); + + if (parsedContainer.jsonObject() == null) { + // Determine version for schema and requirement - this is tricky for a single file + // For now, using default version. A more robust approach might require context. + Version tempVersion = VersionFactory.createVersion(DEFAULT_VERSION); + return new FileValidationResult( + fileName, + tempVersion.isFileRequired(fileName), + true, // File was provided + 0, + tempVersion.getSchema(fileName).toString(), + parsedContainer.originalContent(), + null, // File specific version unknown + Collections.emptyList(), + parsedContainer.parsingErrors() + ); + } else { + return validateFile( + fileName, + Map.of(fileName, parsedContainer.jsonObject()) + ); } - - private Version findVersion(Map fileValidations) { - Set versions = fileValidations.values().stream() - .filter(Objects::nonNull) - .map(FileValidationResult::version) // Version from the file's content itself - .filter(Objects::nonNull) - .collect(Collectors.toSet()); - - if (versions.isEmpty()) { - // If no file provided a version (e.g. all failed parsing or were missing), check gbfs.json specifically - FileValidationResult gbfsFileResult = fileValidations.get("gbfs"); - if (gbfsFileResult != null && gbfsFileResult.fileContents() != null && gbfsFileResult.validatorErrors().isEmpty() && gbfsFileResult.errors().isEmpty()) { - // Attempt to parse gbfs.json again if it was valid but its version wasn't captured by FileValidationResult.version - // This is a bit convoluted; ideally, FileValidationResult.version would be reliably populated. - try { - JSONObject gbfsJson = new JSONObject(gbfsFileResult.fileContents()); - if (gbfsJson.has("version")) { - return VersionFactory.createVersion(gbfsJson.getString("version")); - } - } catch (JSONException e) { - LOG.warn("Could not re-parse gbfs.json for version during findVersion, using default.", e); - } - } - } - - - if (versions.size() > 1) { - LOG.warn("Found multiple versions in files during validation: {}", versions); - // Prioritize gbfs.json version if present among multiple versions - FileValidationResult gbfsFile = fileValidations.get("gbfs"); - if (gbfsFile != null && gbfsFile.version() != null && versions.contains(gbfsFile.version())) { - return VersionFactory.createVersion(gbfsFile.version()); - } + } + + private void handleMissingFiles( + Map fileValidations, + List missingFiles, + Version version + ) { + FileValidator fileValidator = FileValidator.getFileValidator( + version.getVersionString() + ); + missingFiles.forEach(file -> { + FileValidationResult missingResult = fileValidator.validateMissingFile( + file + ); + fileValidations.put(file, missingResult); + }); + } + + private Version findVersion( + Map fileValidations + ) { + Set versions = fileValidations + .values() + .stream() + .filter(Objects::nonNull) + .map(FileValidationResult::version) // Version from the file's content itself + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + + if (versions.isEmpty()) { + // If no file provided a version (e.g. all failed parsing or were missing), check gbfs.json specifically + FileValidationResult gbfsFileResult = fileValidations.get("gbfs"); + if ( + gbfsFileResult != null && + gbfsFileResult.fileContents() != null && + gbfsFileResult.validatorErrors().isEmpty() && + gbfsFileResult.errors().isEmpty() + ) { + // Attempt to parse gbfs.json again if it was valid but its version wasn't captured by FileValidationResult.version + // This is a bit convoluted; ideally, FileValidationResult.version would be reliably populated. + try { + JSONObject gbfsJson = new JSONObject(gbfsFileResult.fileContents()); + if (gbfsJson.has("version")) { + return VersionFactory.createVersion(gbfsJson.getString("version")); + } + } catch (JSONException e) { + LOG.warn( + "Could not re-parse gbfs.json for version during findVersion, using default.", + e + ); } - - - return VersionFactory.createVersion( - versions.stream().findFirst().orElse(DEFAULT_VERSION) - ); + } } - private FileValidationResult validateFile(String feedName, Map feedMap) { - JSONObject feed = feedMap.get(feedName); - if (feed == null) { - return null; - } + if (versions.size() > 1) { + LOG.warn( + "Found multiple versions in files during validation: {}", + versions + ); + // Prioritize gbfs.json version if present among multiple versions + FileValidationResult gbfsFile = fileValidations.get("gbfs"); + if ( + gbfsFile != null && + gbfsFile.version() != null && + versions.contains(gbfsFile.version()) + ) { + return VersionFactory.createVersion(gbfsFile.version()); + } + } - String detectedVersion = feed.has("version") ? feed.getString("version") : "1.0"; - FileValidator fileValidator = FileValidator.getFileValidator(detectedVersion); - return fileValidator.validate(feedName, feedMap); + return VersionFactory.createVersion( + versions.stream().findFirst().orElse(DEFAULT_VERSION) + ); + } + + private FileValidationResult validateFile( + String feedName, + Map feedMap + ) { + JSONObject feed = feedMap.get(feedName); + if (feed == null) { + return null; } + String detectedVersion = feed.has("version") + ? feed.getString("version") + : "1.0"; + FileValidator fileValidator = FileValidator.getFileValidator( + detectedVersion + ); + return fileValidator.validate(feedName, feedMap); + } + + private Map parseFeeds( + Map rawFeeds + ) { + Map feedMap = new HashMap<>(); + rawFeeds.forEach((name, value) -> feedMap.put(name, parseFeed(name, value)) + ); + return feedMap; + } - private Map parseFeeds(Map rawFeeds) { - Map feedMap = new HashMap<>(); - rawFeeds.forEach((name, value) -> feedMap.put(name, parseFeed(name, value))); - return feedMap; + private ParsedFeedContainer parseFeed(String name, InputStream raw) { + String asString; + try ( + BufferedReader reader = new BufferedReader(new InputStreamReader(raw)) + ) { + asString = + reader.lines().collect(Collectors.joining(System.lineSeparator())); + } catch (IOException | UncheckedIOException e) { + LOG.warn( + "IOException while reading feed name={}: {}", + name, + e.getMessage(), + e + ); + return new ParsedFeedContainer( + name, + null, + List.of( + new ValidatorError( + "READ_ERROR", + "IOException reading stream for " + name + ": " + e.getMessage() + ) + ), + null + ); } - private ParsedFeedContainer parseFeed(String name, InputStream raw) { - String asString; - try (BufferedReader reader = new BufferedReader(new InputStreamReader(raw))) { - asString = reader.lines().collect(Collectors.joining(System.lineSeparator())); - } catch (IOException | UncheckedIOException e) { - LOG.warn("IOException while reading feed name={}: {}", name, e.getMessage(), e); - return new ParsedFeedContainer(name, null, List.of(new ValidatorError("READ_ERROR", "IOException reading stream for " + name + ": " + e.getMessage())), null); - } - - try { - return new ParsedFeedContainer(name, new JSONObject(asString), asString); - } catch (JSONException e) { - LOG.warn("Failed to parse json for feed name={} content={}: {}", name, asString, e.getMessage(), e); - return new ParsedFeedContainer(name, null, List.of(new ValidatorError("PARSE_ERROR", e.getMessage())), asString); - } + try { + return new ParsedFeedContainer(name, new JSONObject(asString), asString); + } catch (JSONException e) { + LOG.warn( + "Failed to parse json for feed name={} content={}: {}", + name, + asString, + e.getMessage(), + e + ); + return new ParsedFeedContainer( + name, + null, + List.of(new ValidatorError("PARSE_ERROR", e.getMessage())), + asString + ); } + } } diff --git a/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/validator/URIFormatValidator.java b/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/validator/URIFormatValidator.java index e37df23..9832906 100644 --- a/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/validator/URIFormatValidator.java +++ b/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/validator/URIFormatValidator.java @@ -18,28 +18,28 @@ package org.entur.gbfs.validation.validator; -import org.everit.json.schema.FormatValidator; - import java.net.URI; import java.net.URISyntaxException; import java.util.Optional; +import org.everit.json.schema.FormatValidator; public class URIFormatValidator implements FormatValidator { - @Override - public Optional validate(String subject) { - try { - new URI(subject); - return Optional.empty(); - } catch (URISyntaxException e) { - if (e.getReason().equalsIgnoreCase("expected authority")) { - return Optional.empty(); - } - } - return Optional.of("Invalid URI"); - } - @Override - public String formatName() { - return "uri"; + @Override + public Optional validate(String subject) { + try { + new URI(subject); + return Optional.empty(); + } catch (URISyntaxException e) { + if (e.getReason().equalsIgnoreCase("expected authority")) { + return Optional.empty(); + } } + return Optional.of("Invalid URI"); + } + + @Override + public String formatName() { + return "uri"; + } } diff --git a/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/validator/rules/CustomRuleSchemaPatcher.java b/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/validator/rules/CustomRuleSchemaPatcher.java index 4287b19..5f8c533 100644 --- a/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/validator/rules/CustomRuleSchemaPatcher.java +++ b/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/validator/rules/CustomRuleSchemaPatcher.java @@ -21,21 +21,22 @@ package org.entur.gbfs.validation.validator.rules; import com.jayway.jsonpath.DocumentContext; -import org.json.JSONObject; - import java.util.Map; +import org.json.JSONObject; /** * A CustomRuleSchemaPatcher is capapable of patching the raw json schema, adding custom rules using GBFS * data dynamically */ public interface CustomRuleSchemaPatcher { - - /** - * - * @param rawSchemaDocumentContext - * @param feeds - * @return - */ - DocumentContext addRule(DocumentContext rawSchemaDocumentContext, Map feeds); + /** + * + * @param rawSchemaDocumentContext + * @param feeds + * @return + */ + DocumentContext addRule( + DocumentContext rawSchemaDocumentContext, + Map feeds + ); } diff --git a/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/validator/rules/NoInvalidReferenceToPricingPlansInVehicleStatus.java b/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/validator/rules/NoInvalidReferenceToPricingPlansInVehicleStatus.java index 61d1ce7..089a700 100644 --- a/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/validator/rules/NoInvalidReferenceToPricingPlansInVehicleStatus.java +++ b/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/validator/rules/NoInvalidReferenceToPricingPlansInVehicleStatus.java @@ -22,45 +22,51 @@ import com.jayway.jsonpath.DocumentContext; import com.jayway.jsonpath.JsonPath; +import java.util.Map; import org.json.JSONArray; import org.json.JSONObject; -import java.util.Map; - /** * A vehicle's pricing_plan_id must exist in the system's system_pricing_plan file */ -public class NoInvalidReferenceToPricingPlansInVehicleStatus implements CustomRuleSchemaPatcher { +public class NoInvalidReferenceToPricingPlansInVehicleStatus + implements CustomRuleSchemaPatcher { - public static final String VEHICLE_PRICING_PLAN_ID_SCHEMA_PATH = "$.properties.data.properties.vehicles.items.properties.pricing_plan_id"; - public static final String BIKE_PRICING_PLAN_ID_SCHEMA_PATH = "$.properties.data.properties.bikes.items.properties.pricing_plan_id"; + public static final String VEHICLE_PRICING_PLAN_ID_SCHEMA_PATH = + "$.properties.data.properties.vehicles.items.properties.pricing_plan_id"; + public static final String BIKE_PRICING_PLAN_ID_SCHEMA_PATH = + "$.properties.data.properties.bikes.items.properties.pricing_plan_id"; - private final String fileName; + private final String fileName; - public NoInvalidReferenceToPricingPlansInVehicleStatus(String fileName) { - this.fileName = fileName; - } + public NoInvalidReferenceToPricingPlansInVehicleStatus(String fileName) { + this.fileName = fileName; + } - /** - * Adds an enum to vehicle_status's pricing_plan_id schema with the plan ids from the system_pricing_plan feed - */ - @Override - public DocumentContext addRule(DocumentContext rawSchemaDocumentContext, Map feeds) { - JSONObject pricingPlansFeed = feeds.get("system_pricing_plans"); + /** + * Adds an enum to vehicle_status's pricing_plan_id schema with the plan ids from the system_pricing_plan feed + */ + @Override + public DocumentContext addRule( + DocumentContext rawSchemaDocumentContext, + Map feeds + ) { + JSONObject pricingPlansFeed = feeds.get("system_pricing_plans"); - String requiredPath = VEHICLE_PRICING_PLAN_ID_SCHEMA_PATH; - // backwards compatibility - if (fileName.equals("free_bike_status")) { - requiredPath = BIKE_PRICING_PLAN_ID_SCHEMA_PATH; - } - JSONObject pricingPlanIdSchema = rawSchemaDocumentContext.read(requiredPath); + String requiredPath = VEHICLE_PRICING_PLAN_ID_SCHEMA_PATH; + // backwards compatibility + if (fileName.equals("free_bike_status")) { + requiredPath = BIKE_PRICING_PLAN_ID_SCHEMA_PATH; + } + JSONObject pricingPlanIdSchema = rawSchemaDocumentContext.read( + requiredPath + ); - JSONArray pricingPlanIds = pricingPlansFeed != null - ? JsonPath.parse(pricingPlansFeed).read("$.data.plans[*].plan_id") - : new JSONArray(); - pricingPlanIdSchema.put("enum", pricingPlanIds); + JSONArray pricingPlanIds = pricingPlansFeed != null + ? JsonPath.parse(pricingPlansFeed).read("$.data.plans[*].plan_id") + : new JSONArray(); + pricingPlanIdSchema.put("enum", pricingPlanIds); - return rawSchemaDocumentContext - .set(requiredPath, pricingPlanIdSchema); - } + return rawSchemaDocumentContext.set(requiredPath, pricingPlanIdSchema); + } } diff --git a/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/validator/rules/NoInvalidReferenceToPricingPlansInVehicleTypes.java b/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/validator/rules/NoInvalidReferenceToPricingPlansInVehicleTypes.java index 44cfa2d..bc36834 100644 --- a/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/validator/rules/NoInvalidReferenceToPricingPlansInVehicleTypes.java +++ b/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/validator/rules/NoInvalidReferenceToPricingPlansInVehicleTypes.java @@ -22,37 +22,45 @@ import com.jayway.jsonpath.DocumentContext; import com.jayway.jsonpath.JsonPath; +import java.util.Map; import org.json.JSONArray; import org.json.JSONObject; -import java.util.Map; - /** * A vehicle's default_pricing_plan_id, and all pricing_plan_ids must exist in the system's system_pricing_plan file */ -public class NoInvalidReferenceToPricingPlansInVehicleTypes implements CustomRuleSchemaPatcher { - - public static final String DEFAULT_PRICING_PLAN_ID_SCHEMA_PATH = "$.properties.data.properties.vehicle_types.items.properties.default_pricing_plan_id"; - public static final String PRICING_PLAN_IDS_SCHEMA_PATH = "$.properties.data.properties.vehicle_types.items.properties.pricing_plan_ids.items"; +public class NoInvalidReferenceToPricingPlansInVehicleTypes + implements CustomRuleSchemaPatcher { + public static final String DEFAULT_PRICING_PLAN_ID_SCHEMA_PATH = + "$.properties.data.properties.vehicle_types.items.properties.default_pricing_plan_id"; + public static final String PRICING_PLAN_IDS_SCHEMA_PATH = + "$.properties.data.properties.vehicle_types.items.properties.pricing_plan_ids.items"; - /** - * Adds an enum to vehicle_type's default_pricing_plan_id and pricing_plan_ids schema with the plan ids from the system_pricing_plan feed - */ - @Override - public DocumentContext addRule(DocumentContext rawSchemaDocumentContext, Map feeds) { - JSONObject pricingPlansFeed = feeds.get("system_pricing_plans"); - JSONObject defaultPricingPlanIdSchema = rawSchemaDocumentContext.read(DEFAULT_PRICING_PLAN_ID_SCHEMA_PATH); - JSONObject pricingPlanIdsSchema = rawSchemaDocumentContext.read(PRICING_PLAN_IDS_SCHEMA_PATH); + /** + * Adds an enum to vehicle_type's default_pricing_plan_id and pricing_plan_ids schema with the plan ids from the system_pricing_plan feed + */ + @Override + public DocumentContext addRule( + DocumentContext rawSchemaDocumentContext, + Map feeds + ) { + JSONObject pricingPlansFeed = feeds.get("system_pricing_plans"); + JSONObject defaultPricingPlanIdSchema = rawSchemaDocumentContext.read( + DEFAULT_PRICING_PLAN_ID_SCHEMA_PATH + ); + JSONObject pricingPlanIdsSchema = rawSchemaDocumentContext.read( + PRICING_PLAN_IDS_SCHEMA_PATH + ); - JSONArray pricingPlanIds = pricingPlansFeed != null - ? JsonPath.parse(pricingPlansFeed).read("$.data.plans[*].plan_id") - : new JSONArray(); - defaultPricingPlanIdSchema.put("enum", pricingPlanIds); - pricingPlanIdsSchema.put("enum", pricingPlanIds); + JSONArray pricingPlanIds = pricingPlansFeed != null + ? JsonPath.parse(pricingPlansFeed).read("$.data.plans[*].plan_id") + : new JSONArray(); + defaultPricingPlanIdSchema.put("enum", pricingPlanIds); + pricingPlanIdsSchema.put("enum", pricingPlanIds); - return rawSchemaDocumentContext - .set(DEFAULT_PRICING_PLAN_ID_SCHEMA_PATH, defaultPricingPlanIdSchema) - .set(PRICING_PLAN_IDS_SCHEMA_PATH, pricingPlanIdsSchema); - } + return rawSchemaDocumentContext + .set(DEFAULT_PRICING_PLAN_ID_SCHEMA_PATH, defaultPricingPlanIdSchema) + .set(PRICING_PLAN_IDS_SCHEMA_PATH, pricingPlanIdsSchema); + } } diff --git a/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/validator/rules/NoInvalidReferenceToRegionInStationInformation.java b/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/validator/rules/NoInvalidReferenceToRegionInStationInformation.java index 0bdf125..038de97 100644 --- a/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/validator/rules/NoInvalidReferenceToRegionInStationInformation.java +++ b/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/validator/rules/NoInvalidReferenceToRegionInStationInformation.java @@ -22,11 +22,10 @@ import com.jayway.jsonpath.DocumentContext; import com.jayway.jsonpath.JsonPath; +import java.util.Map; import org.json.JSONArray; import org.json.JSONObject; -import java.util.Map; - /** * References to regions in station_information must exist in the system's system_regions file */ @@ -34,7 +33,7 @@ public class NoInvalidReferenceToRegionInStationInformation implements CustomRuleSchemaPatcher { public static final String REGION_IDS_SCHEMA_PATH = - "$.properties.data.properties.stations.items.properties.region_id"; + "$.properties.data.properties.stations.items.properties.region_id"; /** * Adds an enum to the region_id schema of stations.region_id with the region ids from system_regions.json @@ -46,21 +45,15 @@ public DocumentContext addRule( ) { JSONObject systemRegionsFeed = feeds.get("system_regions"); JSONObject regionIdSchema = rawSchemaDocumentContext.read( - REGION_IDS_SCHEMA_PATH + REGION_IDS_SCHEMA_PATH ); JSONArray regionIds = systemRegionsFeed != null - ? JsonPath - .parse(systemRegionsFeed) - .read("$.data.regions[*].region_id") - : new JSONArray(); + ? JsonPath.parse(systemRegionsFeed).read("$.data.regions[*].region_id") + : new JSONArray(); regionIdSchema.put("enum", regionIds); - return rawSchemaDocumentContext - .set( - REGION_IDS_SCHEMA_PATH, - regionIdSchema - ); + return rawSchemaDocumentContext.set(REGION_IDS_SCHEMA_PATH, regionIdSchema); } } diff --git a/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/validator/rules/NoInvalidReferenceToVehicleTypesInStationStatus.java b/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/validator/rules/NoInvalidReferenceToVehicleTypesInStationStatus.java index e08b597..d66c847 100644 --- a/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/validator/rules/NoInvalidReferenceToVehicleTypesInStationStatus.java +++ b/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/validator/rules/NoInvalidReferenceToVehicleTypesInStationStatus.java @@ -22,37 +22,56 @@ import com.jayway.jsonpath.DocumentContext; import com.jayway.jsonpath.JsonPath; +import java.util.Map; import org.json.JSONArray; import org.json.JSONObject; -import java.util.Map; - /** * References to vehicle types in station_status must exist in the system's vehicle_types file */ -public class NoInvalidReferenceToVehicleTypesInStationStatus implements CustomRuleSchemaPatcher { +public class NoInvalidReferenceToVehicleTypesInStationStatus + implements CustomRuleSchemaPatcher { - public static final String VEHICLE_TYPES_AVAILABLE_VEHICLE_TYPE_ID_SCHEMA_PATH = "$.properties.data.properties.stations.items.properties.vehicle_types_available.items.properties.vehicle_type_id"; - public static final String VEHICLE_DOCKS_AVAILABLE_VEHICLE_TYPE_IDS_SCHEMA_PATH = "$.properties.data.properties.stations.items.properties.vehicle_docks_available.items.properties.vehicle_type_ids.items"; + public static final String VEHICLE_TYPES_AVAILABLE_VEHICLE_TYPE_ID_SCHEMA_PATH = + "$.properties.data.properties.stations.items.properties.vehicle_types_available.items.properties.vehicle_type_id"; + public static final String VEHICLE_DOCKS_AVAILABLE_VEHICLE_TYPE_IDS_SCHEMA_PATH = + "$.properties.data.properties.stations.items.properties.vehicle_docks_available.items.properties.vehicle_type_ids.items"; - /** - * Adds an enum to the vehicle_type_id schema of vehicle_types_available and vehicle_docks_available with the vehilce type ids from vehicle_types.json - */ - @Override - public DocumentContext addRule(DocumentContext rawSchemaDocumentContext, Map feeds) { - JSONObject vehicleTypesFeed = feeds.get("vehicle_types"); - JSONObject vehicleTypesAvailableVehicleTypeIdSchema = rawSchemaDocumentContext.read(VEHICLE_TYPES_AVAILABLE_VEHICLE_TYPE_ID_SCHEMA_PATH); - JSONObject vehicleDocksAvailableVehiecleTypeIdSchema = rawSchemaDocumentContext.read(VEHICLE_DOCKS_AVAILABLE_VEHICLE_TYPE_IDS_SCHEMA_PATH); + /** + * Adds an enum to the vehicle_type_id schema of vehicle_types_available and vehicle_docks_available with the vehilce type ids from vehicle_types.json + */ + @Override + public DocumentContext addRule( + DocumentContext rawSchemaDocumentContext, + Map feeds + ) { + JSONObject vehicleTypesFeed = feeds.get("vehicle_types"); + JSONObject vehicleTypesAvailableVehicleTypeIdSchema = + rawSchemaDocumentContext.read( + VEHICLE_TYPES_AVAILABLE_VEHICLE_TYPE_ID_SCHEMA_PATH + ); + JSONObject vehicleDocksAvailableVehiecleTypeIdSchema = + rawSchemaDocumentContext.read( + VEHICLE_DOCKS_AVAILABLE_VEHICLE_TYPE_IDS_SCHEMA_PATH + ); - // If no vehicle_types feed is defined, then any vehicle_type_id would be invalid - JSONArray vehicleTypeIds = vehicleTypesFeed != null - ? JsonPath.parse(vehicleTypesFeed).read("$.data.vehicle_types[*].vehicle_type_id") - : new JSONArray(); - vehicleTypesAvailableVehicleTypeIdSchema.put("enum", vehicleTypeIds); - vehicleDocksAvailableVehiecleTypeIdSchema.put("enum", vehicleTypeIds); + // If no vehicle_types feed is defined, then any vehicle_type_id would be invalid + JSONArray vehicleTypeIds = vehicleTypesFeed != null + ? JsonPath + .parse(vehicleTypesFeed) + .read("$.data.vehicle_types[*].vehicle_type_id") + : new JSONArray(); + vehicleTypesAvailableVehicleTypeIdSchema.put("enum", vehicleTypeIds); + vehicleDocksAvailableVehiecleTypeIdSchema.put("enum", vehicleTypeIds); - return rawSchemaDocumentContext - .set(VEHICLE_TYPES_AVAILABLE_VEHICLE_TYPE_ID_SCHEMA_PATH, vehicleTypesAvailableVehicleTypeIdSchema) - .set(VEHICLE_DOCKS_AVAILABLE_VEHICLE_TYPE_IDS_SCHEMA_PATH, vehicleDocksAvailableVehiecleTypeIdSchema); - } + return rawSchemaDocumentContext + .set( + VEHICLE_TYPES_AVAILABLE_VEHICLE_TYPE_ID_SCHEMA_PATH, + vehicleTypesAvailableVehicleTypeIdSchema + ) + .set( + VEHICLE_DOCKS_AVAILABLE_VEHICLE_TYPE_IDS_SCHEMA_PATH, + vehicleDocksAvailableVehiecleTypeIdSchema + ); + } } diff --git a/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/validator/rules/NoMissingCurrentRangeMetersInVehicleStatusForMotorizedVehicles.java b/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/validator/rules/NoMissingCurrentRangeMetersInVehicleStatusForMotorizedVehicles.java index e6d36de..38b41b2 100644 --- a/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/validator/rules/NoMissingCurrentRangeMetersInVehicleStatusForMotorizedVehicles.java +++ b/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/validator/rules/NoMissingCurrentRangeMetersInVehicleStatusForMotorizedVehicles.java @@ -20,70 +20,101 @@ package org.entur.gbfs.validation.validator.rules; +import static com.jayway.jsonpath.Criteria.where; + import com.jayway.jsonpath.DocumentContext; import com.jayway.jsonpath.Filter; import com.jayway.jsonpath.JsonPath; -import org.json.JSONArray; -import org.json.JSONObject; - import java.util.List; import java.util.Map; - -import static com.jayway.jsonpath.Criteria.where; +import org.json.JSONArray; +import org.json.JSONObject; /** * It is required to provide the current_range_meters property in vehicle_status for motorized vehicles */ -public class NoMissingCurrentRangeMetersInVehicleStatusForMotorizedVehicles implements CustomRuleSchemaPatcher { +public class NoMissingCurrentRangeMetersInVehicleStatusForMotorizedVehicles + implements CustomRuleSchemaPatcher { + + private final String fileName; + + public NoMissingCurrentRangeMetersInVehicleStatusForMotorizedVehicles( + String fileName + ) { + this.fileName = fileName; + } + + private static final Filter motorizedVehicleTypesFilter = Filter.filter( + where("propulsion_type") + .in(List.of("electric_assist", "electric", "combustion")) + ); + private static final String BIKE_ITEMS_SCHEMA_PATH = + "$.properties.data.properties.bikes.items"; + private static final String VEHICLE_ITEMS_SCHEMA_PATH = + "$.properties.data.properties.vehicles.items"; + + @Override + public DocumentContext addRule( + DocumentContext rawSchemaDocumentContext, + Map feeds + ) { + JSONObject vehicleTypesFeed = feeds.get("vehicle_types"); + + JSONArray motorizedVehicleTypeIds = null; + + if (vehicleTypesFeed != null) { + motorizedVehicleTypeIds = + JsonPath + .parse(vehicleTypesFeed) + .read( + "$.data.vehicle_types[?].vehicle_type_id", + motorizedVehicleTypesFilter + ); + } - private final String fileName; + String schemaPath = VEHICLE_ITEMS_SCHEMA_PATH; - public NoMissingCurrentRangeMetersInVehicleStatusForMotorizedVehicles(String fileName) { - this.fileName = fileName; + if (fileName.equals("free_bike_status")) { + schemaPath = BIKE_ITEMS_SCHEMA_PATH; } - private static final Filter motorizedVehicleTypesFilter = Filter.filter( - where("propulsion_type").in( - List.of( - "electric_assist", "electric", "combustion" + JSONObject bikeItemsSchema = rawSchemaDocumentContext.read(schemaPath); + + if (motorizedVehicleTypeIds != null && !motorizedVehicleTypeIds.isEmpty()) { + bikeItemsSchema.put( + "errorMessage", + new JSONObject() + .put( + "required", + new JSONObject() + .put( + "vehicle_type_id", + "'vehicle_type_id' is required for this vehicle type" + ) + ) + ); + bikeItemsSchema + .put( + "if", + new JSONObject() + .put( + "properties", + new JSONObject() + .put( + "vehicle_type_id", + new JSONObject().put("enum", motorizedVehicleTypeIds) ) ) - ); - private static final String BIKE_ITEMS_SCHEMA_PATH = "$.properties.data.properties.bikes.items"; - private static final String VEHICLE_ITEMS_SCHEMA_PATH = "$.properties.data.properties.vehicles.items"; - - @Override - public DocumentContext addRule(DocumentContext rawSchemaDocumentContext, Map feeds) { - JSONObject vehicleTypesFeed = feeds.get("vehicle_types"); - - JSONArray motorizedVehicleTypeIds = null; - - if (vehicleTypesFeed != null) { - motorizedVehicleTypeIds = JsonPath.parse(vehicleTypesFeed) - .read("$.data.vehicle_types[?].vehicle_type_id", motorizedVehicleTypesFilter); - } - - String schemaPath = VEHICLE_ITEMS_SCHEMA_PATH; - - if (fileName.equals("free_bike_status")) { - schemaPath = BIKE_ITEMS_SCHEMA_PATH; - } - - JSONObject bikeItemsSchema = rawSchemaDocumentContext.read(schemaPath); - - if (motorizedVehicleTypeIds != null && !motorizedVehicleTypeIds.isEmpty()) { - bikeItemsSchema.put("errorMessage", new JSONObject().put("required", new JSONObject().put("vehicle_type_id", "'vehicle_type_id' is required for this vehicle type"))); - bikeItemsSchema - .put("if", - new JSONObject() - .put("properties", new JSONObject().put("vehicle_type_id", new JSONObject().put("enum", motorizedVehicleTypeIds))) - - // "required" so it only trigger "then" when "vehicle_type_id" is present. - .put("required", new JSONArray().put("vehicle_type_id")) - ) - .put("then", new JSONObject().put("required", new JSONArray().put("current_range_meters"))); - } - - return rawSchemaDocumentContext.set(schemaPath, bikeItemsSchema); + // "required" so it only trigger "then" when "vehicle_type_id" is present. + .put("required", new JSONArray().put("vehicle_type_id")) + ) + .put( + "then", + new JSONObject() + .put("required", new JSONArray().put("current_range_meters")) + ); } + + return rawSchemaDocumentContext.set(schemaPath, bikeItemsSchema); + } } diff --git a/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/validator/rules/NoMissingOrInvalidVehicleTypeIdInVehicleStatusWhenVehicleTypesExist.java b/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/validator/rules/NoMissingOrInvalidVehicleTypeIdInVehicleStatusWhenVehicleTypesExist.java index ad72302..1b55a8f 100644 --- a/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/validator/rules/NoMissingOrInvalidVehicleTypeIdInVehicleStatusWhenVehicleTypesExist.java +++ b/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/validator/rules/NoMissingOrInvalidVehicleTypeIdInVehicleStatusWhenVehicleTypesExist.java @@ -22,43 +22,56 @@ import com.jayway.jsonpath.DocumentContext; import com.jayway.jsonpath.JsonPath; +import java.util.Map; import org.json.JSONArray; import org.json.JSONObject; -import java.util.Map; - /** * Bikes / vehicles must refer to a vehicle type when vehicle_types exists */ -public class NoMissingOrInvalidVehicleTypeIdInVehicleStatusWhenVehicleTypesExist implements CustomRuleSchemaPatcher { +public class NoMissingOrInvalidVehicleTypeIdInVehicleStatusWhenVehicleTypesExist + implements CustomRuleSchemaPatcher { - private final String fileName; + private final String fileName; - public NoMissingOrInvalidVehicleTypeIdInVehicleStatusWhenVehicleTypesExist(String fileName) { - this.fileName = fileName; - } + public NoMissingOrInvalidVehicleTypeIdInVehicleStatusWhenVehicleTypesExist( + String fileName + ) { + this.fileName = fileName; + } + + private static final String BIKE_ITEMS_SCHEMA_PATH = + "$.properties.data.properties.bikes.items"; + private static final String VEHICLE_ITEMS_SCHEMA_PATH = + "$.properties.data.properties.vehicles.items"; - private static final String BIKE_ITEMS_SCHEMA_PATH = "$.properties.data.properties.bikes.items"; - private static final String VEHICLE_ITEMS_SCHEMA_PATH = "$.properties.data.properties.vehicles.items"; - @Override - public DocumentContext addRule(DocumentContext rawSchemaDocumentContext, Map feeds) { - JSONObject vehicleTypesFeed = feeds.get("vehicle_types"); + @Override + public DocumentContext addRule( + DocumentContext rawSchemaDocumentContext, + Map feeds + ) { + JSONObject vehicleTypesFeed = feeds.get("vehicle_types"); - String requiredPath = VEHICLE_ITEMS_SCHEMA_PATH; + String requiredPath = VEHICLE_ITEMS_SCHEMA_PATH; - // backwards compatibility - if (fileName.equals("free_bike_status")) { - requiredPath = BIKE_ITEMS_SCHEMA_PATH; - } + // backwards compatibility + if (fileName.equals("free_bike_status")) { + requiredPath = BIKE_ITEMS_SCHEMA_PATH; + } - JSONObject vehicleItemsSchema = rawSchemaDocumentContext.read(requiredPath); - if (vehicleTypesFeed != null) { - vehicleItemsSchema.append("required", "vehicle_type_id"); - } - JSONArray vehicleTypeIds = vehicleTypesFeed != null - ? JsonPath.parse(vehicleTypesFeed).read("$.data.vehicle_types[*].vehicle_type_id") - : new JSONArray(); - vehicleItemsSchema.getJSONObject( "properties").getJSONObject("vehicle_type_id").put("enum", vehicleTypeIds); - return rawSchemaDocumentContext.set(requiredPath, vehicleItemsSchema); + JSONObject vehicleItemsSchema = rawSchemaDocumentContext.read(requiredPath); + if (vehicleTypesFeed != null) { + vehicleItemsSchema.append("required", "vehicle_type_id"); } + JSONArray vehicleTypeIds = vehicleTypesFeed != null + ? JsonPath + .parse(vehicleTypesFeed) + .read("$.data.vehicle_types[*].vehicle_type_id") + : new JSONArray(); + vehicleItemsSchema + .getJSONObject("properties") + .getJSONObject("vehicle_type_id") + .put("enum", vehicleTypeIds); + return rawSchemaDocumentContext.set(requiredPath, vehicleItemsSchema); + } } diff --git a/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/validator/rules/NoMissingStoreUriInSystemInformation.java b/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/validator/rules/NoMissingStoreUriInSystemInformation.java index a0f17c7..7b496ae 100644 --- a/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/validator/rules/NoMissingStoreUriInSystemInformation.java +++ b/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/validator/rules/NoMissingStoreUriInSystemInformation.java @@ -22,83 +22,109 @@ import com.jayway.jsonpath.DocumentContext; import com.jayway.jsonpath.JsonPath; +import java.util.Map; import org.json.JSONArray; import org.json.JSONObject; -import java.util.Map; - /** * It is required to provide ios and android store uris in system_information if vehicle_status * or station_information has ios and android rental uris respectively */ -public class NoMissingStoreUriInSystemInformation implements CustomRuleSchemaPatcher { - - private static final String DATA_REQUIRED_SCHEMA_PATH = "$.properties.data.required"; - private static final String RENTAL_APPS_SCHEMA_PATH = "$.properties.data.properties.rental_apps"; - - - private final String vehicleStatusFileName; - - - public NoMissingStoreUriInSystemInformation(String vehicleStatusFileName) { - this.vehicleStatusFileName = vehicleStatusFileName; +public class NoMissingStoreUriInSystemInformation + implements CustomRuleSchemaPatcher { + + private static final String DATA_REQUIRED_SCHEMA_PATH = + "$.properties.data.required"; + private static final String RENTAL_APPS_SCHEMA_PATH = + "$.properties.data.properties.rental_apps"; + + private final String vehicleStatusFileName; + + public NoMissingStoreUriInSystemInformation(String vehicleStatusFileName) { + this.vehicleStatusFileName = vehicleStatusFileName; + } + + @Override + public DocumentContext addRule( + DocumentContext rawSchemaDocumentContext, + Map feeds + ) { + boolean hasIosRentalUris = false; + boolean hasAndroidRentalUris = false; + + JSONObject vehicleStatusFeed = feeds.get(vehicleStatusFileName); + + if (vehicleStatusFeed != null) { + String vehiclesKey = vehicleStatusFileName.equals("vehicle_status") + ? "vehicles" + : "bikes"; + + if ( + !( + (JSONArray) JsonPath + .parse(vehicleStatusFeed) + .read("$.data." + vehiclesKey + "[:1].rental_uris.ios") + ).isEmpty() + ) { + hasIosRentalUris = true; + } + + if ( + !( + (JSONArray) JsonPath + .parse(vehicleStatusFeed) + .read("$.data." + vehiclesKey + "[:1].rental_uris.android") + ).isEmpty() + ) { + hasAndroidRentalUris = true; + } } - @Override - public DocumentContext addRule(DocumentContext rawSchemaDocumentContext, Map feeds) { - boolean hasIosRentalUris = false; - boolean hasAndroidRentalUris = false; - - JSONObject vehicleStatusFeed = feeds.get(vehicleStatusFileName); - - if (vehicleStatusFeed != null) { - String vehiclesKey = vehicleStatusFileName.equals("vehicle_status") ? "vehicles" : "bikes"; - - if (!((JSONArray) JsonPath.parse(vehicleStatusFeed) - .read("$.data." + vehiclesKey + "[:1].rental_uris.ios")).isEmpty()) { - hasIosRentalUris = true; - } - - if (!((JSONArray) JsonPath.parse(vehicleStatusFeed) - .read("$.data." + vehiclesKey + "[:1].rental_uris.android")).isEmpty()) { - hasAndroidRentalUris = true; - } - } - - JSONObject stationInformationFeed = feeds.get("station_information"); - - if (stationInformationFeed != null) { - if (!((JSONArray) JsonPath.parse(stationInformationFeed) - .read("$.data.stations[:1].rental_uris.ios")).isEmpty()) { - hasIosRentalUris = true; - } - - if (!((JSONArray) JsonPath.parse(stationInformationFeed) - .read("$.data.stations[:1].rental_uris.android")).isEmpty()) { - hasAndroidRentalUris = true; - } - } - - if (hasIosRentalUris || hasAndroidRentalUris) { - - JSONArray systemInformationDataRequiredSchema = rawSchemaDocumentContext.read(DATA_REQUIRED_SCHEMA_PATH); - systemInformationDataRequiredSchema.put("rental_apps"); - - JSONObject rentalAppsSchema = rawSchemaDocumentContext.read(RENTAL_APPS_SCHEMA_PATH); - JSONArray rentalAppRequired = new JSONArray(); + JSONObject stationInformationFeed = feeds.get("station_information"); + + if (stationInformationFeed != null) { + if ( + !( + (JSONArray) JsonPath + .parse(stationInformationFeed) + .read("$.data.stations[:1].rental_uris.ios") + ).isEmpty() + ) { + hasIosRentalUris = true; + } + + if ( + !( + (JSONArray) JsonPath + .parse(stationInformationFeed) + .read("$.data.stations[:1].rental_uris.android") + ).isEmpty() + ) { + hasAndroidRentalUris = true; + } + } + if (hasIosRentalUris || hasAndroidRentalUris) { + JSONArray systemInformationDataRequiredSchema = + rawSchemaDocumentContext.read(DATA_REQUIRED_SCHEMA_PATH); + systemInformationDataRequiredSchema.put("rental_apps"); - if (hasIosRentalUris) { - rentalAppRequired.put("ios"); - } + JSONObject rentalAppsSchema = rawSchemaDocumentContext.read( + RENTAL_APPS_SCHEMA_PATH + ); + JSONArray rentalAppRequired = new JSONArray(); - if (hasAndroidRentalUris) { - rentalAppRequired.put("android"); - } + if (hasIosRentalUris) { + rentalAppRequired.put("ios"); + } - rentalAppsSchema.put("required", rentalAppRequired); - } + if (hasAndroidRentalUris) { + rentalAppRequired.put("android"); + } - return rawSchemaDocumentContext; + rentalAppsSchema.put("required", rentalAppRequired); } + + return rawSchemaDocumentContext; + } } diff --git a/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/validator/rules/NoMissingVehicleTypesAvailableWhenVehicleTypesExists.java b/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/validator/rules/NoMissingVehicleTypesAvailableWhenVehicleTypesExists.java index 0173f3d..0630807 100644 --- a/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/validator/rules/NoMissingVehicleTypesAvailableWhenVehicleTypesExists.java +++ b/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/validator/rules/NoMissingVehicleTypesAvailableWhenVehicleTypesExists.java @@ -21,28 +21,37 @@ package org.entur.gbfs.validation.validator.rules; import com.jayway.jsonpath.DocumentContext; +import java.util.Map; import org.json.JSONArray; import org.json.JSONObject; -import java.util.Map; - /** * It is required to list available vehicle types in station_status when vehicle_types file exists */ -public class NoMissingVehicleTypesAvailableWhenVehicleTypesExists implements CustomRuleSchemaPatcher { +public class NoMissingVehicleTypesAvailableWhenVehicleTypesExists + implements CustomRuleSchemaPatcher { - public static final String STATION_ITEMS_REQUIRED_SCHEMA_PATH = "$.properties.data.properties.stations.items.required"; + public static final String STATION_ITEMS_REQUIRED_SCHEMA_PATH = + "$.properties.data.properties.stations.items.required"; - /** - * Adds vehicle_types_available to list of required properties on stations in station_status - */ - @Override - public DocumentContext addRule(DocumentContext rawSchemaDocumentContext, Map feeds) { - JSONObject vehicleTypesFeed = feeds.get("vehicle_types"); - JSONArray stationItemsRequiredSchema = rawSchemaDocumentContext.read(STATION_ITEMS_REQUIRED_SCHEMA_PATH); - if (vehicleTypesFeed != null) { - stationItemsRequiredSchema.put("vehicle_types_available"); - } - return rawSchemaDocumentContext.set(STATION_ITEMS_REQUIRED_SCHEMA_PATH, stationItemsRequiredSchema); + /** + * Adds vehicle_types_available to list of required properties on stations in station_status + */ + @Override + public DocumentContext addRule( + DocumentContext rawSchemaDocumentContext, + Map feeds + ) { + JSONObject vehicleTypesFeed = feeds.get("vehicle_types"); + JSONArray stationItemsRequiredSchema = rawSchemaDocumentContext.read( + STATION_ITEMS_REQUIRED_SCHEMA_PATH + ); + if (vehicleTypesFeed != null) { + stationItemsRequiredSchema.put("vehicle_types_available"); } + return rawSchemaDocumentContext.set( + STATION_ITEMS_REQUIRED_SCHEMA_PATH, + stationItemsRequiredSchema + ); + } } diff --git a/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/validator/versions/AbstractVersion.java b/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/validator/versions/AbstractVersion.java index 1cc610f..ec0c370 100644 --- a/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/validator/versions/AbstractVersion.java +++ b/gbfs-validator-java/src/main/java/org/entur/gbfs/validation/validator/versions/AbstractVersion.java @@ -25,6 +25,14 @@ import com.jayway.jsonpath.spi.json.JsonProvider; import com.jayway.jsonpath.spi.mapper.JsonOrgMappingProvider; import com.jayway.jsonpath.spi.mapper.MappingProvider; +import java.io.InputStream; +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import org.entur.gbfs.validation.validator.FileValidator; import org.entur.gbfs.validation.validator.URIFormatValidator; import org.entur.gbfs.validation.validator.rules.CustomRuleSchemaPatcher; @@ -36,121 +44,150 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.InputStream; -import java.util.Collections; -import java.util.EnumSet; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; - public abstract class AbstractVersion implements Version { - private static final Logger logger = LoggerFactory.getLogger(AbstractVersion.class); - private final String versionString; - private final List feeds; - private final Map schemas = new ConcurrentHashMap<>(); - private final Map> customRules; - - static { - Configuration.setDefaults(new Configuration.Defaults() { - final JsonProvider jsonProvider = new JsonOrgJsonProvider(); - final MappingProvider mappingProvider = new JsonOrgMappingProvider(); - - @Override - public JsonProvider jsonProvider() { - return jsonProvider; - } - - @Override - public Set