5757import java .util .List ;
5858import java .util .Map ;
5959
60+ /**
61+ * Service implementation for GBFS validation API operations.
62+ * Handles validation requests by loading GBFS files and running them through the validator.
63+ */
6064@ Service
6165public class ValidateApiDelegateHandler implements ValidateApiDelegate {
6266 private static final Logger logger = LoggerFactory .getLogger (ValidateApiDelegateHandler .class );
6367
6468 private final Loader loader ;
6569
70+ /**
71+ * Creates a new validation handler.
72+ *
73+ * @param loader the GBFS file loader to use
74+ */
6675 public ValidateApiDelegateHandler (Loader loader ) {
6776 this .loader = loader ;
6877 }
6978
79+ /**
80+ * Cleans up resources when the service is destroyed.
81+ * Closes the loader's HTTP client and thread pool.
82+ */
7083 @ PreDestroy
7184 public void destroy () {
7285 try {
@@ -78,6 +91,12 @@ public void destroy() {
7891 }
7992 }
8093
94+ /**
95+ * Validates a GBFS feed by loading all files and running validation.
96+ *
97+ * @param validatePostRequest the validation request containing feed URL and optional authentication
98+ * @return validation results with file-level errors and system errors
99+ */
81100 @ Override
82101 public ResponseEntity <org .entur .gbfs .validator .api .model .ValidationResult > validatePost (ValidatePostRequest validatePostRequest ) {
83102 logger .debug ("Received request for url: {}" , validatePostRequest .getFeedUrl ());
@@ -105,13 +124,8 @@ public ResponseEntity<org.entur.gbfs.validator.api.model.ValidationResult> valid
105124
106125 logger .debug ("Loaded files: {}" , allLoadedFiles .size ());
107126
108- // Group loaded files by language. For files without a language (e.g. gbfs.json, or if loader doesn't set it),
109- // use a default key or handle them as appropriate. loader.load() should populate language for pre-v3.
110- // For v3+, language is typically null at the LoadedFile stage for gbfs.json itself.
111127 Multimap <String , LoadedFile > filesByLanguage = MultimapBuilder .hashKeys ().arrayListValues ().build ();
112128 for (LoadedFile loadedFile : allLoadedFiles ) {
113- // Use a placeholder if language is null to ensure they are processed.
114- // gbfs.json (discovery file) itself might have null language.
115129 String langKey = loadedFile .language () != null ? loadedFile .language () : "default_lang" ;
116130 filesByLanguage .put (langKey , loadedFile );
117131 }
@@ -122,58 +136,48 @@ public ResponseEntity<org.entur.gbfs.validator.api.model.ValidationResult> valid
122136 filesByLanguage .asMap ().forEach ((languageKey , loadedFilesForLang ) -> {
123137 logger .debug ("Processing language group: {}" , languageKey );
124138 Map <String , InputStream > validatorInputMap = new HashMap <>();
125- // Keep track of LoadedFile objects for this language group to pass to mapping
126139 List <LoadedFile > currentLanguageLoadedFiles = new ArrayList <>(loadedFilesForLang );
127140
128141 for (LoadedFile file : currentLanguageLoadedFiles ) {
129- // Only try to validate files that have content.
130- // Files with system errors from loader might have null fileContents.
131142 if (file .fileContents () != null ) {
132143 validatorInputMap .put (file .fileName (), file .fileContents ());
133144 }
134- // Note: urlMap is not used in the refined plan for mapFiles directly,
135- // as LoadedFile itself contains the URL.
136145 }
137146
138147 ValidationResult internalValidationResult = validator .validate (validatorInputMap );
139148
140149 resultsPerLanguage .add (
141150 mapValidationResult (
142151 internalValidationResult ,
143- currentLanguageLoadedFiles , // Pass the list of LoadedFile for this language
144- "default_lang" .equals (languageKey ) ? null : languageKey // Pass actual language, or null
152+ currentLanguageLoadedFiles ,
153+ "default_lang" .equals (languageKey ) ? null : languageKey
145154 )
146155 );
147156 logger .debug ("Processed {} files for language group: {}" , currentLanguageLoadedFiles .size (), languageKey );
148157 });
149158
150- // merge the list of ValidationResult into a single validation result
151159 return ResponseEntity .ok (
152160 mergeValidationResults (resultsPerLanguage )
153161 );
154162
155163 } catch (IOException e ) {
156- // Consider mapping IOExceptions from loader to a SystemError in the response too
157164 logger .error ("IOException during validation process" , e );
158- // Depending on desired API behavior, could return a 500 with a SystemError
159- throw new RuntimeException (e ); // Or handle more gracefully
165+ throw new RuntimeException (e );
160166 }
161167 }
162168
163169 private org .entur .gbfs .validator .api .model .ValidationResult mergeValidationResults (List <org .entur .gbfs .validator .api .model .ValidationResult > results ) {
164170 if (results .isEmpty ()) {
165- // Handle case with no results, perhaps due to total load failure of discovery file
166171 org .entur .gbfs .validator .api .model .ValidationResult emptyApiResult = new org .entur .gbfs .validator .api .model .ValidationResult ();
167172 ValidationResultSummary emptySummary = new ValidationResultSummary ();
168- emptySummary .setValidatorVersion ("2.0.30-SNAPSHOT" ); // TODO: Inject this
173+ emptySummary .setValidatorVersion ("2.0.30-SNAPSHOT" );
169174 emptySummary .setFiles (new ArrayList <>());
170175 emptyApiResult .setSummary (emptySummary );
171176 return emptyApiResult ;
172177 }
173178
174179 org .entur .gbfs .validator .api .model .ValidationResult mergedResult = new org .entur .gbfs .validator .api .model .ValidationResult ();
175180 ValidationResultSummary summary = new ValidationResultSummary ();
176- // Assuming validatorVersion is consistent or taking from the first result
177181 summary .setValidatorVersion (results .get (0 ).getSummary ().getValidatorVersion ());
178182 List <GbfsFile > allFiles = new ArrayList <>();
179183 results .forEach (result -> {
@@ -182,32 +186,20 @@ private org.entur.gbfs.validator.api.model.ValidationResult mergeValidationResul
182186 }
183187 });
184188
185- // Dedup files if necessary, though current logic iterates by language group,
186- // so gbfs.json might appear multiple times if language key was 'default_lang' for it.
187- // For now, simple aggregation. A more sophisticated merge might be needed if files are duplicated across "language" groups.
188- // A simple distinct by URL or name could be:
189- // List<GbfsFile> distinctFiles = allFiles.stream().filter(distinctByKey(GbfsFile::getUrl)).toList();
190- // summary.setFiles(distinctFiles);
191- summary .setFiles (allFiles ); // Using simple aggregation for now.
189+ summary .setFiles (allFiles );
192190
193191 mergedResult .setSummary (summary );
194192 return mergedResult ;
195193 }
196- /*
197- private static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) {
198- Map<Object, Boolean> seen = new ConcurrentHashMap<>();
199- return t -> seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null;
200- }
201- */
202194
203195
204196 private org .entur .gbfs .validator .api .model .ValidationResult mapValidationResult (
205197 ValidationResult internalValidationResult ,
206- List <LoadedFile > loadedFilesForLanguage , // Now includes LoadedFile list
207- String language // Actual language string, can be null
198+ List <LoadedFile > loadedFilesForLanguage ,
199+ String language
208200 ) {
209201 ValidationResultSummary validationResultSummary = new ValidationResultSummary ();
210- validationResultSummary .setValidatorVersion ("2.0.30-SNAPSHOT" ); // TODO inject this value
202+ validationResultSummary .setValidatorVersion ("2.0.30-SNAPSHOT" );
211203
212204 validationResultSummary .setFiles (
213205 mapFiles (loadedFilesForLanguage , internalValidationResult .files (), language )
@@ -221,7 +213,7 @@ private org.entur.gbfs.validator.api.model.ValidationResult mapValidationResult(
221213 private List <GbfsFile > mapFiles (
222214 List <LoadedFile > loadedFilesForLanguage ,
223215 Map <String , FileValidationResult > validatedFileResultsMap ,
224- String language // Actual language string, can be null for gbfs.json
216+ String language
225217 ) {
226218 List <GbfsFile > apiGbfsFiles = new ArrayList <>();
227219
@@ -232,7 +224,6 @@ private List<GbfsFile> mapFiles(
232224
233225 List <SystemError > combinedApiSystemErrors = new ArrayList <>();
234226
235- // System errors from loader
236227 List <LoaderError > loaderSystemErrors = loadedFile .loaderErrors ();
237228 if (loaderSystemErrors != null && !loaderSystemErrors .isEmpty ()) {
238229 combinedApiSystemErrors .addAll (mapLoaderSystemErrorsToApi (loaderSystemErrors ));
@@ -241,29 +232,22 @@ private List<GbfsFile> mapFiles(
241232 FileValidationResult validationResult = validatedFileResultsMap .get (loadedFile .fileName ());
242233
243234 if (validationResult != null ) {
244- // File was processed by validator
245235 apiFile .setSchema (validationResult .schema ());
246236 apiFile .setVersion (validationResult .version ());
247237 apiFile .setErrors (mapFileValidationErrors (validationResult .errors ()));
248238
249- // Add system errors from validator (parsing errors)
250239 List <ValidatorError > validatorSystemErrors = validationResult .validatorErrors ();
251240 if (validatorSystemErrors != null && !validatorSystemErrors .isEmpty ()) {
252241 combinedApiSystemErrors .addAll (mapValidatorSystemErrorsToApi (validatorSystemErrors ));
253242 }
254243 } else {
255- // File was not processed by validator (e.g. content was null due to load error)
256- // or validator skipped it. Schema/Version might be unknown.
257- // Validation errors are empty.
258244 apiFile .setErrors (new ArrayList <>());
259245 }
260246
261247 apiFile .setSystemErrors (combinedApiSystemErrors );
262248
263- // Set language - gbfs.json (discovery file) typically has no language.
264- // Other files get the language of their group.
265249 if (loadedFile .fileName ().equals ("gbfs.json" ) || loadedFile .fileName ().equals ("gbfs" )) {
266- apiFile .setLanguage (null ); // Explicitly null for discovery
250+ apiFile .setLanguage (null );
267251 } else {
268252 apiFile .setLanguage (JsonNullable .of (language ));
269253 }
0 commit comments