Skip to content

Commit 1a3de7d

Browse files
author
Landon Reed
authored
Merge pull request #133 from conveyal/fix-fare-rule-no-route
Editor: Allow null values for foreign ref fields or required fields where permitted
2 parents 54aff02 + d10aff3 commit 1a3de7d

File tree

7 files changed

+276
-31
lines changed

7 files changed

+276
-31
lines changed

src/main/java/com/conveyal/gtfs/loader/JdbcGtfsSnapshotter.java

+8-3
Original file line numberDiff line numberDiff line change
@@ -176,8 +176,7 @@ private TableLoadResult copy (Table table, boolean createIndexes) {
176176
*/
177177
private TableLoadResult createScheduleExceptionsTable() {
178178
// check to see if the schedule_exceptions table exists
179-
boolean scheduleExceptionsTableExists = feedIdToSnapshot != null &&
180-
tableExists(feedIdToSnapshot, "schedule_exceptions");
179+
boolean scheduleExceptionsTableExists = tableExists(feedIdToSnapshot, "schedule_exceptions");
181180
String scheduleExceptionsTableName = tablePrefix + "schedule_exceptions";
182181

183182
if (scheduleExceptionsTableExists) {
@@ -271,7 +270,11 @@ private TableLoadResult createScheduleExceptionsTable() {
271270

272271
// determine if we appear to be working with a calendar_dates-only feed.
273272
// If so, we must also add dummy entries to the calendar table
274-
if (!tableExists(feedIdToSnapshot, "calendar") && calendarDatesReader.getRowCount() > 0) {
273+
if (
274+
feedIdToSnapshot != null &&
275+
!tableExists(feedIdToSnapshot, "calendar") &&
276+
calendarDatesReader.getRowCount() > 0
277+
) {
275278
sql = String.format(
276279
"insert into %s (service_id, description, start_date, end_date, " +
277280
"monday, tuesday, wednesday, thursday, friday, saturday, sunday)" +
@@ -322,6 +325,8 @@ private TableLoadResult createScheduleExceptionsTable() {
322325
* Helper method to determine if a table exists within a namespace.
323326
*/
324327
private boolean tableExists(String namespace, String tableName) {
328+
// Preempt SQL check with null check of either namespace or table name.
329+
if (namespace == null || tableName == null) return false;
325330
try {
326331
// This statement is postgres-specific.
327332
PreparedStatement tableExistsStatement = connection.prepareStatement(

src/main/java/com/conveyal/gtfs/loader/JdbcTableWriter.java

+11-4
Original file line numberDiff line numberDiff line change
@@ -292,10 +292,13 @@ private void setStatementParameters(ObjectNode jsonObject, Table table, Prepared
292292
continue;
293293
}
294294
JsonNode value = jsonObject.get(field.name);
295-
// LOG.info("{}={}", field.name, value);
295+
LOG.debug("{}={}", field.name, value);
296296
try {
297297
if (value == null || value.isNull()) {
298-
if (field.isRequired()) {
298+
if (field.isRequired() && !field.isEmptyValuePermitted()) {
299+
// Only register the field as missing if the value is null, the field is required, and empty
300+
// values are not permitted. For example, a null value for fare_attributes#transfers should not
301+
// trigger a missing field exception.
299302
missingFieldNames.add(field.name);
300303
continue;
301304
}
@@ -318,7 +321,7 @@ private void setStatementParameters(ObjectNode jsonObject, Table table, Prepared
318321
// FIXME: This is a hack to get arrival and departure time into the right format. Because the UI
319322
// currently returns them as seconds since midnight rather than the Field-defined format HH:MM:SS.
320323
try {
321-
if (value == null ||value.isNull()) {
324+
if (value == null || value.isNull()) {
322325
if (field.isRequired()) {
323326
missingFieldNames.add(field.name);
324327
continue;
@@ -421,7 +424,11 @@ private void updateChildTable(ArrayNode subEntities, Integer id, boolean isCreat
421424
// field statement above.
422425
for (Field field : subTable.specFields()) {
423426
if (field.referenceTable != null && !field.referenceTable.name.equals(specTable.name)) {
424-
referencesPerTable.put(field.referenceTable, subEntity.get(field.name).asText());
427+
JsonNode refValueNode = subEntity.get(field.name);
428+
// Skip over references that are null but not required (e.g., route_id in fare_rules).
429+
if (refValueNode.isNull() && !field.isRequired()) continue;
430+
String refValue = refValueNode.asText();
431+
referencesPerTable.put(field.referenceTable, refValue);
425432
}
426433
}
427434
// Insert new sub-entity.

src/test/java/com/conveyal/gtfs/GTFSTest.java

+7-3
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
import static org.junit.Assert.assertThat;
3434

3535
/**
36-
* A test suite for the GTFS Class
36+
* A test suite for the {@link GTFS} Class.
3737
*/
3838
public class GTFSTest {
3939
private final ByteArrayOutputStream outContent = new ByteArrayOutputStream();
@@ -570,7 +570,9 @@ private void assertThatPersistenceExpectationRecordWasFound(int numRecordsSearch
570570
);
571571
}
572572

573-
// a helper class to verify that data got stored in a particular table
573+
/**
574+
* A helper class to verify that data got stored in a particular table.
575+
*/
574576
private class PersistenceExpectation {
575577
public String tableName;
576578
// each persistence expectation has an array of record expectations which all must reference a single row
@@ -590,7 +592,9 @@ private enum ExpectedFieldType {
590592
DOUBLE, STRING
591593
}
592594

593-
// a helper class to verify that data got stored in a particular record
595+
/**
596+
* A helper class to verify that data got stored in a particular record.
597+
*/
594598
private class RecordExpectation {
595599
public double acceptedDelta;
596600
public double doubleExpectation;

src/test/java/com/conveyal/gtfs/loader/JDBCTableWriterTest.java

+188-21
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package com.conveyal.gtfs.loader;
22

33
import com.conveyal.gtfs.TestUtils;
4+
import com.conveyal.gtfs.util.FareDTO;
5+
import com.conveyal.gtfs.util.FareRuleDTO;
46
import com.conveyal.gtfs.util.FeedInfoDTO;
57
import com.conveyal.gtfs.util.InvalidNamespaceException;
8+
import com.conveyal.gtfs.util.RouteDTO;
69
import com.fasterxml.jackson.databind.ObjectMapper;
710
import org.junit.AfterClass;
811
import org.junit.BeforeClass;
@@ -28,14 +31,19 @@ public class JDBCTableWriterTest {
2831
private static String testDBName;
2932
private static DataSource testDataSource;
3033
private static String testNamespace;
34+
private static final ObjectMapper mapper = new ObjectMapper();
35+
36+
private static JdbcTableWriter createTestTableWriter (Table table) throws InvalidNamespaceException {
37+
return new JdbcTableWriter(table, testDataSource, testNamespace);
38+
}
3139

3240
@BeforeClass
3341
public static void setUpClass() throws SQLException {
3442
// create a new database
3543
testDBName = TestUtils.generateNewDB();
3644
String dbConnectionUrl = String.format("jdbc:postgresql://localhost/%s", testDBName);
3745
testDataSource = createDataSource (dbConnectionUrl, null, null);
38-
LOG.info("creating feeds table because it isn't automtically generated unless you import a feed");
46+
LOG.info("creating feeds table because it isn't automatically generated unless you import a feed");
3947
Connection connection = testDataSource.getConnection();
4048
connection.createStatement()
4149
.execute("create table if not exists feeds (namespace varchar primary key, md5 varchar, " +
@@ -50,8 +58,10 @@ public static void setUpClass() throws SQLException {
5058
}
5159

5260
@Test
53-
public void canCreateUpdateAndDeleteFeedinfoEntities () throws IOException, SQLException, InvalidNamespaceException {
54-
ObjectMapper mapper = new ObjectMapper();
61+
public void canCreateUpdateAndDeleteFeedInfoEntities() throws IOException, SQLException, InvalidNamespaceException {
62+
// Store Table and Class values for use in test.
63+
final Table feedInfoTable = Table.FEED_INFO;
64+
final Class<FeedInfoDTO> feedInfoDTOClass = FeedInfoDTO.class;
5565

5666
// create new object to be saved
5767
FeedInfoDTO feedInfoInput = new FeedInfoDTO();
@@ -63,13 +73,13 @@ public void canCreateUpdateAndDeleteFeedinfoEntities () throws IOException, SQLE
6373
feedInfoInput.default_route_type = "3";
6474

6575
// convert object to json and save it
66-
JdbcTableWriter createTableWriter = new JdbcTableWriter(Table.FEED_INFO, testDataSource, testNamespace);
76+
JdbcTableWriter createTableWriter = createTestTableWriter(feedInfoTable);
6777
String createOutput = createTableWriter.create(mapper.writeValueAsString(feedInfoInput), true);
68-
LOG.info("create output:");
78+
LOG.info("create {} output:", feedInfoTable.name);
6979
LOG.info(createOutput);
7080

7181
// parse output
72-
FeedInfoDTO createdFeedInfo = mapper.readValue(createOutput, FeedInfoDTO.class);
82+
FeedInfoDTO createdFeedInfo = mapper.readValue(createOutput, feedInfoDTOClass);
7383

7484
// make sure saved data matches expected data
7585
assertThat(createdFeedInfo.feed_publisher_name, equalTo(publisherName));
@@ -79,45 +89,39 @@ public void canCreateUpdateAndDeleteFeedinfoEntities () throws IOException, SQLE
7989
createdFeedInfo.feed_publisher_name = updatedPublisherName;
8090

8191
// covert object to json and save it
82-
JdbcTableWriter updateTableWriter = new JdbcTableWriter(Table.FEED_INFO, testDataSource, testNamespace);
92+
JdbcTableWriter updateTableWriter = createTestTableWriter(feedInfoTable);
8393
String updateOutput = updateTableWriter.update(
8494
createdFeedInfo.id,
8595
mapper.writeValueAsString(createdFeedInfo),
8696
true
8797
);
88-
LOG.info("update output:");
98+
LOG.info("update {} output:", feedInfoTable.name);
8999
LOG.info(updateOutput);
90100

91-
FeedInfoDTO updatedFeedInfoDTO = mapper.readValue(updateOutput, FeedInfoDTO.class);
101+
FeedInfoDTO updatedFeedInfoDTO = mapper.readValue(updateOutput, feedInfoDTOClass);
92102

93103
// make sure saved data matches expected data
94104
assertThat(updatedFeedInfoDTO.feed_publisher_name, equalTo(updatedPublisherName));
95105

96106
// try to delete record
97-
JdbcTableWriter deleteTableWriter = new JdbcTableWriter(Table.FEED_INFO, testDataSource, testNamespace);
107+
JdbcTableWriter deleteTableWriter = createTestTableWriter(feedInfoTable);
98108
int deleteOutput = deleteTableWriter.delete(
99109
createdFeedInfo.id,
100110
true
101111
);
102-
LOG.info("delete output:");
103-
LOG.info(updateOutput);
112+
LOG.info("deleted {} records from {}", deleteOutput, feedInfoTable.name);
104113

105114
// make sure record does not exist in DB
106-
String sql = String.format(
115+
assertThatSqlQueryYieldsZeroRows(String.format(
107116
"select * from %s.%s where id=%d",
108117
testNamespace,
109-
Table.FEED_INFO.name,
118+
feedInfoTable.name,
110119
createdFeedInfo.id
111-
);
112-
LOG.info(sql);
113-
ResultSet rs = testDataSource.getConnection().prepareStatement(sql).executeQuery();
114-
assertThat(rs.getFetchSize(), equalTo(0));
120+
));
115121
}
116122

117123
@Test
118124
public void canPreventSQLInjection() throws IOException, SQLException, InvalidNamespaceException {
119-
ObjectMapper mapper = new ObjectMapper();
120-
121125
// create new object to be saved
122126
FeedInfoDTO feedInfoInput = new FeedInfoDTO();
123127
String publisherName = "' OR 1 = 1; SELECT '1";
@@ -128,7 +132,7 @@ public void canPreventSQLInjection() throws IOException, SQLException, InvalidNa
128132
feedInfoInput.default_route_type = "3";
129133

130134
// convert object to json and save it
131-
JdbcTableWriter createTableWriter = new JdbcTableWriter(Table.FEED_INFO, testDataSource, testNamespace);
135+
JdbcTableWriter createTableWriter = createTestTableWriter(Table.FEED_INFO);
132136
String createOutput = createTableWriter.create(mapper.writeValueAsString(feedInfoInput), true);
133137
LOG.info("create output:");
134138
LOG.info(createOutput);
@@ -140,6 +144,169 @@ public void canPreventSQLInjection() throws IOException, SQLException, InvalidNa
140144
assertThat(createdFeedInfo.feed_publisher_name, equalTo(publisherName));
141145
}
142146

147+
@Test
148+
public void canCreateUpdateAndDeleteFares() throws IOException, SQLException, InvalidNamespaceException {
149+
// Store Table and Class values for use in test.
150+
final Table fareTable = Table.FARE_ATTRIBUTES;
151+
final Class<FareDTO> fareDTOClass = FareDTO.class;
152+
153+
// create new object to be saved
154+
FareDTO fareInput = new FareDTO();
155+
String fareId = "2A";
156+
fareInput.fare_id = fareId;
157+
fareInput.currency_type = "USD";
158+
fareInput.price = 2.50;
159+
fareInput.agency_id = "RTA";
160+
fareInput.payment_method = 0;
161+
// Empty value should be permitted for transfers and transfer_duration
162+
fareInput.transfers = null;
163+
fareInput.transfer_duration = null;
164+
FareRuleDTO fareRuleInput = new FareRuleDTO();
165+
// Fare ID should be assigned to "child entity" by editor automatically.
166+
fareRuleInput.fare_id = null;
167+
fareRuleInput.route_id = null;
168+
// FIXME There is currently no check for valid zone_id values in contains_id, origin_id, and destination_id.
169+
fareRuleInput.contains_id = "any";
170+
fareRuleInput.origin_id = "value";
171+
fareRuleInput.destination_id = "permitted";
172+
fareInput.fare_rules = new FareRuleDTO[]{fareRuleInput};
173+
174+
// convert object to json and save it
175+
JdbcTableWriter createTableWriter = createTestTableWriter(fareTable);
176+
String createOutput = createTableWriter.create(mapper.writeValueAsString(fareInput), true);
177+
LOG.info("create {} output:", fareTable.name);
178+
LOG.info(createOutput);
179+
180+
// parse output
181+
FareDTO createdFare = mapper.readValue(createOutput, fareDTOClass);
182+
183+
// make sure saved data matches expected data
184+
assertThat(createdFare.fare_id, equalTo(fareId));
185+
assertThat(createdFare.fare_rules[0].fare_id, equalTo(fareId));
186+
187+
// try to update record
188+
String updatedFareId = "3B";
189+
createdFare.fare_id = updatedFareId;
190+
191+
// covert object to json and save it
192+
JdbcTableWriter updateTableWriter = createTestTableWriter(fareTable);
193+
String updateOutput = updateTableWriter.update(
194+
createdFare.id,
195+
mapper.writeValueAsString(createdFare),
196+
true
197+
);
198+
LOG.info("update {} output:", fareTable.name);
199+
LOG.info(updateOutput);
200+
201+
FareDTO updatedFareDTO = mapper.readValue(updateOutput, fareDTOClass);
202+
203+
// make sure saved data matches expected data
204+
assertThat(updatedFareDTO.fare_id, equalTo(updatedFareId));
205+
assertThat(updatedFareDTO.fare_rules[0].fare_id, equalTo(updatedFareId));
206+
207+
// try to delete record
208+
JdbcTableWriter deleteTableWriter = createTestTableWriter(fareTable);
209+
int deleteOutput = deleteTableWriter.delete(
210+
createdFare.id,
211+
true
212+
);
213+
LOG.info("deleted {} records from {}", deleteOutput, fareTable.name);
214+
215+
// make sure fare_attributes record does not exist in DB
216+
assertThatSqlQueryYieldsZeroRows(String.format(
217+
"select * from %s.%s where id=%d",
218+
testNamespace,
219+
fareTable.name,
220+
createdFare.id
221+
));
222+
223+
// make sure fare_rules record does not exist in DB
224+
assertThatSqlQueryYieldsZeroRows(String.format(
225+
"select * from %s.%s where id=%d",
226+
testNamespace,
227+
Table.FARE_RULES.name,
228+
createdFare.fare_rules[0].id
229+
));
230+
}
231+
232+
private void assertThatSqlQueryYieldsZeroRows(String sql) throws SQLException {
233+
assertThatSqlQueryYieldsRowCount(sql, 0);
234+
}
235+
236+
private void assertThatSqlQueryYieldsRowCount(String sql, int expectedRowCount) throws SQLException {
237+
LOG.info(sql);
238+
ResultSet resultSet = testDataSource.getConnection().prepareStatement(sql).executeQuery();
239+
assertThat(resultSet.getFetchSize(), equalTo(expectedRowCount));
240+
}
241+
242+
@Test
243+
public void canCreateUpdateAndDeleteRoutes() throws IOException, SQLException, InvalidNamespaceException {
244+
// Store Table and Class values for use in test.
245+
final Table routeTable = Table.ROUTES;
246+
final Class<RouteDTO> routeDTOClass = RouteDTO.class;
247+
248+
// create new object to be saved
249+
RouteDTO routeInput = new RouteDTO();
250+
String routeId = "500";
251+
routeInput.route_id = routeId;
252+
routeInput.agency_id = "RTA";
253+
// Empty value should be permitted for transfers and transfer_duration
254+
routeInput.route_short_name = "500";
255+
routeInput.route_long_name = "Hollingsworth";
256+
routeInput.route_type = 3;
257+
258+
// convert object to json and save it
259+
JdbcTableWriter createTableWriter = createTestTableWriter(routeTable);
260+
String createOutput = createTableWriter.create(mapper.writeValueAsString(routeInput), true);
261+
LOG.info("create {} output:", routeTable.name);
262+
LOG.info(createOutput);
263+
264+
// parse output
265+
RouteDTO createdRoute = mapper.readValue(createOutput, routeDTOClass);
266+
267+
// make sure saved data matches expected data
268+
assertThat(createdRoute.route_id, equalTo(routeId));
269+
// TODO: Verify with a SQL query that the database now contains the created data (we may need to use the same
270+
// db connection to do this successfully?)
271+
272+
// try to update record
273+
String updatedRouteId = "600";
274+
createdRoute.route_id = updatedRouteId;
275+
276+
// covert object to json and save it
277+
JdbcTableWriter updateTableWriter = createTestTableWriter(routeTable);
278+
String updateOutput = updateTableWriter.update(
279+
createdRoute.id,
280+
mapper.writeValueAsString(createdRoute),
281+
true
282+
);
283+
LOG.info("update {} output:", routeTable.name);
284+
LOG.info(updateOutput);
285+
286+
RouteDTO updatedRouteDTO = mapper.readValue(updateOutput, routeDTOClass);
287+
288+
// make sure saved data matches expected data
289+
assertThat(updatedRouteDTO.route_id, equalTo(updatedRouteId));
290+
// TODO: Verify with a SQL query that the database now contains the updated data (we may need to use the same
291+
// db connection to do this successfully?)
292+
293+
// try to delete record
294+
JdbcTableWriter deleteTableWriter = createTestTableWriter(routeTable);
295+
int deleteOutput = deleteTableWriter.delete(
296+
createdRoute.id,
297+
true
298+
);
299+
LOG.info("deleted {} records from {}", deleteOutput, routeTable.name);
300+
301+
// make sure route record does not exist in DB
302+
assertThatSqlQueryYieldsZeroRows(String.format(
303+
"select * from %s.%s where id=%d",
304+
testNamespace,
305+
routeTable.name,
306+
createdRoute.id
307+
));
308+
}
309+
143310
@AfterClass
144311
public static void tearDownClass() {
145312
TestUtils.dropDB(testDBName);

0 commit comments

Comments
 (0)