13
13
import com .conveyal .file .FileUtils ;
14
14
import com .conveyal .gtfs .GTFSCache ;
15
15
import com .conveyal .gtfs .GTFSFeed ;
16
- import com .conveyal .gtfs .error .GTFSError ;
17
16
import com .conveyal .gtfs .error .GeneralError ;
18
17
import com .conveyal .gtfs .model .Stop ;
19
18
import com .conveyal .gtfs .validator .PostLoadValidator ;
20
19
import com .conveyal .osmlib .Node ;
21
20
import com .conveyal .osmlib .OSM ;
22
- import com .conveyal .r5 .analyst .progress .ProgressInputStream ;
23
21
import com .conveyal .r5 .analyst .cluster .TransportNetworkConfig ;
22
+ import com .conveyal .r5 .analyst .progress .ProgressInputStream ;
24
23
import com .conveyal .r5 .analyst .progress .Task ;
25
24
import com .conveyal .r5 .streets .OSMCache ;
26
25
import com .conveyal .r5 .util .ExceptionUtils ;
@@ -81,6 +80,7 @@ public BundleController (BackendComponents components) {
81
80
public void registerEndpoints (Service sparkService ) {
82
81
sparkService .path ("/api/bundle" , () -> {
83
82
sparkService .get ("" , this ::getBundles , toJson );
83
+ sparkService .get ("/:_id/config" , this ::getBundleConfig , toJson );
84
84
sparkService .get ("/:_id" , this ::getBundle , toJson );
85
85
sparkService .post ("" , this ::create , toJson );
86
86
sparkService .put ("/:_id" , this ::update , toJson );
@@ -110,15 +110,13 @@ private Bundle create (Request req, Response res) {
110
110
try {
111
111
bundle .name = files .get ("bundleName" ).get (0 ).getString ("UTF-8" );
112
112
bundle .regionId = files .get ("regionId" ).get (0 ).getString ("UTF-8" );
113
-
114
113
if (files .get ("osmId" ) != null ) {
115
114
bundle .osmId = files .get ("osmId" ).get (0 ).getString ("UTF-8" );
116
115
Bundle bundleWithOsm = Persistence .bundles .find (QueryBuilder .start ("osmId" ).is (bundle .osmId ).get ()).next ();
117
116
if (bundleWithOsm == null ) {
118
117
throw AnalysisServerException .badRequest ("Selected OSM does not exist." );
119
118
}
120
119
}
121
-
122
120
if (files .get ("feedGroupId" ) != null ) {
123
121
bundle .feedGroupId = files .get ("feedGroupId" ).get (0 ).getString ("UTF-8" );
124
122
Bundle bundleWithFeed = Persistence .bundles .find (QueryBuilder .start ("feedGroupId" ).is (bundle .feedGroupId ).get ()).next ();
@@ -135,6 +133,13 @@ private Bundle create (Request req, Response res) {
135
133
bundle .feedsComplete = bundleWithFeed .feedsComplete ;
136
134
bundle .totalFeeds = bundleWithFeed .totalFeeds ;
137
135
}
136
+ if (files .get ("config" ) != null ) {
137
+ // Validation by deserializing into a model class instance. Unknown fields are ignored to
138
+ // allow sending config to custom or experimental workers with features unknown to the backend.
139
+ // The fields specifying OSM and GTFS IDs are not expected here. They will be ignored and overwritten.
140
+ String configString = files .get ("config" ).get (0 ).getString ();
141
+ bundle .config = JsonUtil .objectMapper .readValue (configString , TransportNetworkConfig .class );
142
+ }
138
143
UserPermissions userPermissions = UserPermissions .from (req );
139
144
bundle .accessGroup = userPermissions .accessGroup ;
140
145
bundle .createdBy = userPermissions .email ;
@@ -274,15 +279,19 @@ private Bundle create (Request req, Response res) {
274
279
return bundle ;
275
280
}
276
281
282
+ /** SIDE EFFECTS: This method will change the field bundle.config before writing it. */
277
283
private void writeNetworkConfigToCache (Bundle bundle ) throws IOException {
278
- TransportNetworkConfig networkConfig = new TransportNetworkConfig ();
279
- networkConfig .osmId = bundle .osmId ;
280
- networkConfig .gtfsIds = bundle .feeds .stream ().map (f -> f .bundleScopedFeedId ).collect (Collectors .toList ());
281
-
284
+ // If the user specified additional network configuration options, they should already be in bundle.config.
285
+ // If no custom options were specified, we start with a fresh, empty instance.
286
+ if (bundle .config == null ) {
287
+ bundle .config = new TransportNetworkConfig ();
288
+ }
289
+ // This will overwrite and override any inconsistent osm and gtfs IDs that were mistakenly supplied by the user.
290
+ bundle .config .osmId = bundle .osmId ;
291
+ bundle .config .gtfsIds = bundle .feeds .stream ().map (f -> f .bundleScopedFeedId ).collect (Collectors .toList ());
282
292
String configFileName = bundle ._id + ".json" ;
283
293
File configFile = FileUtils .createScratchFile ("json" );
284
- JsonUtil .objectMapper .writeValue (configFile , networkConfig );
285
-
294
+ JsonUtil .objectMapper .writeValue (configFile , bundle .config );
286
295
FileStorageKey key = new FileStorageKey (BUNDLES , configFileName );
287
296
fileStorage .moveIntoStorage (key , configFile );
288
297
}
@@ -312,6 +321,31 @@ private Bundle getBundle (Request req, Response res) {
312
321
return bundle ;
313
322
}
314
323
324
+ /**
325
+ * There are two copies of the Bundle/Network config: one in the Bundle entry in the database and one in a JSON
326
+ * file (obtainable by the workers). This method always reads the one in the file, which has been around longer
327
+ * and is considered the definitive source of truth. The entry in the database is a newer addition and has only
328
+ * been around since September 2024.
329
+ */
330
+ private TransportNetworkConfig getBundleConfig (Request request , Response res ) {
331
+ // Unfortunately this mimics logic in TransportNetworkCache. Deduplicate in a static utility method?
332
+ String id = GTFSCache .cleanId (request .params ("_id" ));
333
+ FileStorageKey key = new FileStorageKey (BUNDLES , id , "json" );
334
+ File networkConfigFile = fileStorage .getFile (key );
335
+ if (!networkConfigFile .exists ()) {
336
+ throw AnalysisServerException .notFound ("Bundle configuration file could not be found." );
337
+ }
338
+
339
+ // Unlike in the worker, we expect the backend to have a model field for every known network/bundle option.
340
+ // Therefore, use the default objectMapper that does not tolerate unknown fields.
341
+ try {
342
+ return JsonUtil .objectMapper .readValue (networkConfigFile , TransportNetworkConfig .class );
343
+ } catch (Exception exception ) {
344
+ LOG .error ("Exception deserializing stored network config" , exception );
345
+ return null ;
346
+ }
347
+ }
348
+
315
349
private Collection <Bundle > getBundles (Request req , Response res ) {
316
350
return Persistence .bundles .findPermittedForQuery (req );
317
351
}
0 commit comments