Skip to content

Commit 7062076

Browse files
author
Landon Reed
authored
Merge pull request #164 from conveyal/update-trip-shape_id
Update trip#shape_id and frequency stop_times on pattern updates
2 parents be32a87 + 37fb468 commit 7062076

13 files changed

+597
-88
lines changed

Diff for: src/main/java/com/conveyal/gtfs/graphql/GraphQLGtfsSchema.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,7 @@ public class GraphQLGtfsSchema {
381381
.field(MapFetcher.field("shape_dist_traveled", GraphQLFloat))
382382
.field(MapFetcher.field("drop_off_type", GraphQLInt))
383383
.field(MapFetcher.field("pickup_type", GraphQLInt))
384+
.field(MapFetcher.field("stop_sequence", GraphQLInt))
384385
.field(MapFetcher.field("timepoint", GraphQLInt))
385386
// FIXME: This will only returns a list with one stop entity (unless there is a referential integrity issue)
386387
// Should this be modified to be an object, rather than a list?
@@ -390,7 +391,6 @@ public class GraphQLGtfsSchema {
390391
.dataFetcher(new JDBCFetcher("stops", "stop_id"))
391392
.build()
392393
)
393-
.field(MapFetcher.field("stop_sequence", GraphQLInt))
394394
.build();
395395

396396
/**

Diff for: src/main/java/com/conveyal/gtfs/loader/JdbcTableWriter.java

+271-64
Large diffs are not rendered by default.

Diff for: src/main/java/com/conveyal/gtfs/loader/Table.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ public Table (String name, Class<? extends Entity> entityClass, Requirement requ
163163
new ColorField("route_text_color", OPTIONAL),
164164
// Editor fields below.
165165
new ShortField("publicly_visible", EDITOR, 1),
166+
// wheelchair_accessible is an exemplar field applied to all trips on a route.
166167
new ShortField("wheelchair_accessible", EDITOR, 2).permitEmptyValue(),
167168
new IntegerField("route_sort_order", OPTIONAL, 0, Integer.MAX_VALUE),
168169
// Status values are In progress (0), Pending approval (1), and Approved (2).
@@ -196,7 +197,8 @@ public Table (String name, Class<? extends Entity> entityClass, Requirement requ
196197
new StringField("pattern_id", REQUIRED),
197198
new StringField("route_id", REQUIRED).isReferenceTo(ROUTES),
198199
new StringField("name", OPTIONAL),
199-
// Editor-specific fields
200+
// Editor-specific fields.
201+
// direction_id and shape_id are exemplar fields applied to all trips for a pattern.
200202
new ShortField("direction_id", EDITOR, 1),
201203
new ShortField("use_frequency", EDITOR, 1),
202204
new StringField("shape_id", EDITOR).isReferenceTo(SHAPES)

Diff for: src/test/java/com/conveyal/gtfs/loader/JDBCTableWriterTest.java

+192-20
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
package com.conveyal.gtfs.loader;
22

33
import com.conveyal.gtfs.TestUtils;
4+
import com.conveyal.gtfs.util.CalendarDTO;
45
import com.conveyal.gtfs.util.FareDTO;
56
import com.conveyal.gtfs.util.FareRuleDTO;
67
import com.conveyal.gtfs.util.FeedInfoDTO;
8+
import com.conveyal.gtfs.util.FrequencyDTO;
79
import com.conveyal.gtfs.util.InvalidNamespaceException;
10+
import com.conveyal.gtfs.util.PatternDTO;
11+
import com.conveyal.gtfs.util.PatternStopDTO;
812
import com.conveyal.gtfs.util.RouteDTO;
13+
import com.conveyal.gtfs.util.ShapePointDTO;
14+
import com.conveyal.gtfs.util.StopDTO;
15+
import com.conveyal.gtfs.util.StopTimeDTO;
16+
import com.conveyal.gtfs.util.TripDTO;
917
import com.fasterxml.jackson.databind.ObjectMapper;
1018
import org.junit.AfterClass;
1119
import org.junit.BeforeClass;
@@ -24,22 +32,31 @@
2432
import static org.hamcrest.Matchers.equalTo;
2533
import static org.junit.Assert.assertThat;
2634

35+
/**
36+
* This class contains CRUD tests for {@link JdbcTableWriter} (i.e., editing GTFS entities in the RDBMS). Set up
37+
* consists of creating a scratch database and an empty feed snapshot, which is the necessary starting condition
38+
* for building a GTFS feed from scratch. It then runs the various CRUD tests and finishes by dropping the database
39+
* (even if tests fail).
40+
*/
2741
public class JDBCTableWriterTest {
2842

2943
private static final Logger LOG = LoggerFactory.getLogger(JDBCTableWriterTest.class);
3044

3145
private static String testDBName;
3246
private static DataSource testDataSource;
3347
private static String testNamespace;
48+
private static String simpleServiceId = "1";
49+
private static String firstStopId = "1";
50+
private static String lastStopId = "2";
3451
private static final ObjectMapper mapper = new ObjectMapper();
3552

3653
private static JdbcTableWriter createTestTableWriter (Table table) throws InvalidNamespaceException {
3754
return new JdbcTableWriter(table, testDataSource, testNamespace);
3855
}
3956

4057
@BeforeClass
41-
public static void setUpClass() throws SQLException {
42-
// create a new database
58+
public static void setUpClass() throws SQLException, IOException, InvalidNamespaceException {
59+
// Create a new database
4360
testDBName = TestUtils.generateNewDB();
4461
String dbConnectionUrl = String.format("jdbc:postgresql://localhost/%s", testDBName);
4562
testDataSource = createDataSource (dbConnectionUrl, null, null);
@@ -51,10 +68,13 @@ public static void setUpClass() throws SQLException {
5168
"snapshot_of varchar)");
5269
connection.commit();
5370
LOG.info("feeds table created");
54-
55-
// create an empty snapshot to create a new namespace and all the tables
71+
// Create an empty snapshot to create a new namespace and all the tables
5672
FeedLoadResult result = makeSnapshot(null, testDataSource);
5773
testNamespace = result.uniqueIdentifier;
74+
// Create a service calendar and two stops, both of which are necessary to perform pattern and trip tests.
75+
createWeekdayCalendar(simpleServiceId, "20180103", "20180104");
76+
createSimpleStop(firstStopId, "First Stop", 34.2222, -87.333);
77+
createSimpleStop(lastStopId, "Last Stop", 34.2233, -87.334);
5878
}
5979

6080
@Test
@@ -120,6 +140,10 @@ public void canCreateUpdateAndDeleteFeedInfoEntities() throws IOException, SQLEx
120140
));
121141
}
122142

143+
/**
144+
* Ensure that potentially malicious SQL injection is sanitized properly during create operations.
145+
* TODO: We might should perform this check on multiple entities and for update and/or delete operations.
146+
*/
123147
@Test
124148
public void canPreventSQLInjection() throws IOException, SQLException, InvalidNamespaceException {
125149
// create new object to be saved
@@ -248,23 +272,8 @@ public void canCreateUpdateAndDeleteRoutes() throws IOException, SQLException, I
248272
final Class<RouteDTO> routeDTOClass = RouteDTO.class;
249273

250274
// create new object to be saved
251-
RouteDTO routeInput = new RouteDTO();
252275
String routeId = "500";
253-
routeInput.route_id = routeId;
254-
routeInput.agency_id = "RTA";
255-
// Empty value should be permitted for transfers and transfer_duration
256-
routeInput.route_short_name = "500";
257-
routeInput.route_long_name = "Hollingsworth";
258-
routeInput.route_type = 3;
259-
260-
// convert object to json and save it
261-
JdbcTableWriter createTableWriter = createTestTableWriter(routeTable);
262-
String createOutput = createTableWriter.create(mapper.writeValueAsString(routeInput), true);
263-
LOG.info("create {} output:", routeTable.name);
264-
LOG.info(createOutput);
265-
266-
// parse output
267-
RouteDTO createdRoute = mapper.readValue(createOutput, routeDTOClass);
276+
RouteDTO createdRoute = createSimpleTestRoute(routeId, "RTA", "500", "Hollingsworth", 3);
268277

269278
// make sure saved data matches expected data
270279
assertThat(createdRoute.route_id, equalTo(routeId));
@@ -309,6 +318,169 @@ public void canCreateUpdateAndDeleteRoutes() throws IOException, SQLException, I
309318
));
310319
}
311320

321+
/**
322+
* Create and store a simple route for testing.
323+
*/
324+
private static RouteDTO createSimpleTestRoute(String routeId, String agencyId, String shortName, String longName, int routeType) throws InvalidNamespaceException, IOException, SQLException {
325+
RouteDTO input = new RouteDTO();
326+
input.route_id = routeId;
327+
input.agency_id = agencyId;
328+
// Empty value should be permitted for transfers and transfer_duration
329+
input.route_short_name = shortName;
330+
input.route_long_name = longName;
331+
input.route_type = routeType;
332+
// convert object to json and save it
333+
JdbcTableWriter createTableWriter = createTestTableWriter(Table.ROUTES);
334+
String output = createTableWriter.create(mapper.writeValueAsString(input), true);
335+
LOG.info("create {} output:", Table.ROUTES.name);
336+
LOG.info(output);
337+
// parse output
338+
return mapper.readValue(output, RouteDTO.class);
339+
}
340+
341+
/**
342+
* Creates a pattern by first creating a route and then a pattern for that route.
343+
*/
344+
private static PatternDTO createSimpleRouteAndPattern(String routeId, String patternId, String name) throws InvalidNamespaceException, SQLException, IOException {
345+
// Create new route
346+
createSimpleTestRoute(routeId, "RTA", "500", "Hollingsworth", 3);
347+
// Create new pattern for route
348+
PatternDTO input = new PatternDTO();
349+
input.pattern_id = patternId;
350+
input.route_id = routeId;
351+
input.name = name;
352+
input.use_frequency = 0;
353+
input.shapes = new ShapePointDTO[]{};
354+
input.pattern_stops = new PatternStopDTO[]{};
355+
// Write the pattern to the database
356+
JdbcTableWriter createPatternWriter = createTestTableWriter(Table.PATTERNS);
357+
String output = createPatternWriter.create(mapper.writeValueAsString(input), true);
358+
LOG.info("create {} output:", Table.PATTERNS.name);
359+
LOG.info(output);
360+
// Parse output
361+
return mapper.readValue(output, PatternDTO.class);
362+
}
363+
364+
/**
365+
* Test that a frequency trip entry CANNOT be added for a timetable-based pattern. Expects an exception to be thrown.
366+
*/
367+
@Test(expected = IllegalStateException.class)
368+
public void cannotCreateFrequencyForTimetablePattern() throws InvalidNamespaceException, IOException, SQLException {
369+
PatternDTO simplePattern = createSimpleRouteAndPattern("900", "8", "The Loop");
370+
TripDTO tripInput = constructFrequencyTrip(simplePattern.pattern_id, simplePattern.route_id, 6 * 60 * 60);
371+
JdbcTableWriter createTripWriter = createTestTableWriter(Table.TRIPS);
372+
createTripWriter.create(mapper.writeValueAsString(tripInput), true);
373+
}
374+
375+
/**
376+
* Checks that creating a frequency trip functions properly. This also updates the pattern to include pattern stops,
377+
* which is a prerequisite for creating a frequency trip with stop times.
378+
*/
379+
@Test
380+
public void canCreateUpdateAndDeleteFrequencyTripForFrequencyPattern() throws IOException, SQLException, InvalidNamespaceException {
381+
// Store Table and Class values for use in test.
382+
final Table tripsTable = Table.TRIPS;
383+
int startTime = 6 * 60 * 60;
384+
PatternDTO simplePattern = createSimpleRouteAndPattern("1000", "9", "The Line");
385+
TripDTO tripInput = constructFrequencyTrip(simplePattern.pattern_id, simplePattern.route_id, startTime);
386+
JdbcTableWriter createTripWriter = createTestTableWriter(tripsTable);
387+
// Update pattern with pattern stops, set to use frequencies, and TODO shape points
388+
JdbcTableWriter patternUpdater = createTestTableWriter(Table.PATTERNS);
389+
simplePattern.use_frequency = 1;
390+
simplePattern.pattern_stops = new PatternStopDTO[]{
391+
new PatternStopDTO(simplePattern.pattern_id, firstStopId, 0),
392+
new PatternStopDTO(simplePattern.pattern_id, lastStopId, 1)
393+
};
394+
String updatedPatternOutput = patternUpdater.update(simplePattern.id, mapper.writeValueAsString(simplePattern), true);
395+
LOG.info("Updated pattern output: {}", updatedPatternOutput);
396+
// Create new trip for the pattern
397+
String createTripOutput = createTripWriter.create(mapper.writeValueAsString(tripInput), true);
398+
TripDTO createdTrip = mapper.readValue(createTripOutput, TripDTO.class);
399+
// Update trip
400+
// TODO: Add update and delete tests for updating pattern stops, stop_times, and frequencies.
401+
String updatedTripId = "100A";
402+
createdTrip.trip_id = updatedTripId;
403+
JdbcTableWriter updateTripWriter = createTestTableWriter(tripsTable);
404+
String updateTripOutput = updateTripWriter.update(tripInput.id, mapper.writeValueAsString(createdTrip), true);
405+
TripDTO updatedTrip = mapper.readValue(updateTripOutput, TripDTO.class);
406+
// Check that saved data matches expected data
407+
assertThat(updatedTrip.frequencies[0].start_time, equalTo(startTime));
408+
assertThat(updatedTrip.trip_id, equalTo(updatedTripId));
409+
// Delete trip record
410+
JdbcTableWriter deleteTripWriter = createTestTableWriter(tripsTable);
411+
int deleteOutput = deleteTripWriter.delete(
412+
createdTrip.id,
413+
true
414+
);
415+
LOG.info("deleted {} records from {}", deleteOutput, tripsTable.name);
416+
// Check that record does not exist in DB
417+
assertThatSqlQueryYieldsZeroRows(
418+
String.format(
419+
"select * from %s.%s where id=%d",
420+
testNamespace,
421+
tripsTable.name,
422+
createdTrip.id
423+
));
424+
}
425+
426+
/**
427+
* Construct (without writing to the database) a trip with a frequency entry.
428+
*/
429+
private TripDTO constructFrequencyTrip(String patternId, String routeId, int startTime) {
430+
TripDTO tripInput = new TripDTO();
431+
tripInput.pattern_id = patternId;
432+
tripInput.route_id = routeId;
433+
tripInput.service_id = simpleServiceId;
434+
tripInput.stop_times = new StopTimeDTO[]{
435+
new StopTimeDTO(firstStopId, 0, 0, 0),
436+
new StopTimeDTO(lastStopId, 60, 60, 1)
437+
};
438+
FrequencyDTO frequency = new FrequencyDTO();
439+
frequency.start_time = startTime;
440+
frequency.end_time = 9 * 60 * 60;
441+
frequency.headway_secs = 15 * 60;
442+
tripInput.frequencies = new FrequencyDTO[]{frequency};
443+
return tripInput;
444+
}
445+
446+
/**
447+
* Create and store a simple stop entity.
448+
*/
449+
private static StopDTO createSimpleStop(String stopId, String stopName, double latitude, double longitude) throws InvalidNamespaceException, IOException, SQLException {
450+
JdbcTableWriter createStopWriter = new JdbcTableWriter(Table.STOPS, testDataSource, testNamespace);
451+
StopDTO input = new StopDTO();
452+
input.stop_id = stopId;
453+
input.stop_name = stopName;
454+
input.stop_lat = latitude;
455+
input.stop_lon = longitude;
456+
String output = createStopWriter.create(mapper.writeValueAsString(input), true);
457+
LOG.info("create {} output:", Table.STOPS.name);
458+
LOG.info(output);
459+
return mapper.readValue(output, StopDTO.class);
460+
}
461+
462+
/**
463+
* Create and store a simple calendar that runs on each weekday.
464+
*/
465+
private static CalendarDTO createWeekdayCalendar(String serviceId, String startDate, String endDate) throws IOException, SQLException, InvalidNamespaceException {
466+
JdbcTableWriter createCalendarWriter = new JdbcTableWriter(Table.CALENDAR, testDataSource, testNamespace);
467+
CalendarDTO calendarInput = new CalendarDTO();
468+
calendarInput.service_id = serviceId;
469+
calendarInput.monday = 1;
470+
calendarInput.tuesday = 1;
471+
calendarInput.wednesday = 1;
472+
calendarInput.thursday = 1;
473+
calendarInput.friday = 1;
474+
calendarInput.saturday = 0;
475+
calendarInput.sunday = 0;
476+
calendarInput.start_date = startDate;
477+
calendarInput.end_date = endDate;
478+
String output = createCalendarWriter.create(mapper.writeValueAsString(calendarInput), true);
479+
LOG.info("create {} output:", Table.CALENDAR.name);
480+
LOG.info(output);
481+
return mapper.readValue(output, CalendarDTO.class);
482+
}
483+
312484
@AfterClass
313485
public static void tearDownClass() {
314486
TestUtils.dropDB(testDBName);
+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.conveyal.gtfs.util;
2+
3+
public class CalendarDTO {
4+
public Integer id;
5+
public String service_id;
6+
public Integer monday;
7+
public Integer tuesday;
8+
public Integer wednesday;
9+
public Integer thursday;
10+
public Integer friday;
11+
public Integer saturday;
12+
public Integer sunday;
13+
public String start_date;
14+
public String end_date;
15+
public String description;
16+
}
+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.conveyal.gtfs.util;
2+
3+
public class FrequencyDTO {
4+
public String trip_id;
5+
public Integer start_time;
6+
public Integer end_time;
7+
public Integer headway_secs;
8+
public Integer exact_times;
9+
}

Diff for: src/test/java/com/conveyal/gtfs/util/PatternDTO.java

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.conveyal.gtfs.util;
2+
3+
public class PatternDTO {
4+
public Integer id;
5+
public String pattern_id;
6+
public String shape_id;
7+
public String route_id;
8+
public Integer direction_id;
9+
public Integer use_frequency;
10+
public String name;
11+
public PatternStopDTO[] pattern_stops;
12+
public ShapePointDTO[] shapes;
13+
}
+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.conveyal.gtfs.util;
2+
3+
public class PatternStopDTO {
4+
public Integer id;
5+
public String pattern_id;
6+
public String stop_id;
7+
public Integer default_travel_time;
8+
public Integer default_dwell_time;
9+
public Double shape_dist_traveled;
10+
public Integer drop_off_type;
11+
public Integer pickup_type;
12+
public Integer stop_sequence;
13+
public Integer timepoint;
14+
15+
public PatternStopDTO (String patternId, String stopId, int stopSequence) {
16+
pattern_id = patternId;
17+
stop_id = stopId;
18+
stop_sequence = stopSequence;
19+
}
20+
}
+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package com.conveyal.gtfs.util;
2+
3+
// TODO add fields
4+
public class ShapePointDTO {
5+
6+
}

Diff for: src/test/java/com/conveyal/gtfs/util/StopDTO.java

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.conveyal.gtfs.util;
2+
3+
public class StopDTO {
4+
public Integer id;
5+
public String stop_id;
6+
public String stop_name;
7+
public String stop_code;
8+
public String stop_desc;
9+
public Double stop_lon;
10+
public Double stop_lat;
11+
public String zone_id;
12+
public String stop_url;
13+
public String stop_timezone;
14+
public String parent_station;
15+
public Integer location_type;
16+
public Integer wheelchair_boarding;
17+
}

0 commit comments

Comments
 (0)