diff --git a/main/src/main/java/org/mobilitydata/gtfsvalidator/table/GtfsRouteNetworkSchema.java b/main/src/main/java/org/mobilitydata/gtfsvalidator/table/GtfsRouteNetworkSchema.java new file mode 100644 index 0000000000..fb14d1257f --- /dev/null +++ b/main/src/main/java/org/mobilitydata/gtfsvalidator/table/GtfsRouteNetworkSchema.java @@ -0,0 +1,17 @@ +package org.mobilitydata.gtfsvalidator.table; + +import org.mobilitydata.gtfsvalidator.annotation.*; + +@GtfsTable("route_networks.txt") +public interface GtfsRouteNetworkSchema extends GtfsEntity { + @FieldType(FieldTypeEnum.ID) + @ForeignKey(table = "routes.txt", field = "route_id") + @Required + @PrimaryKey + String routeId(); + + @FieldType(FieldTypeEnum.ID) + @ForeignKey(table = "networks.txt", field = "network_id") + @Required + String networkId(); +} diff --git a/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/NetworkIdConsistencyValidator.java b/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/NetworkIdConsistencyValidator.java new file mode 100644 index 0000000000..610ce4d4fb --- /dev/null +++ b/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/NetworkIdConsistencyValidator.java @@ -0,0 +1,81 @@ +package org.mobilitydata.gtfsvalidator.validator; + +import static org.mobilitydata.gtfsvalidator.notice.SeverityLevel.ERROR; + +import javax.inject.Inject; +import org.mobilitydata.gtfsvalidator.annotation.GtfsValidationNotice; +import org.mobilitydata.gtfsvalidator.annotation.GtfsValidator; +import org.mobilitydata.gtfsvalidator.notice.NoticeContainer; +import org.mobilitydata.gtfsvalidator.notice.ValidationNotice; +import org.mobilitydata.gtfsvalidator.table.*; + +@GtfsValidator +public class NetworkIdConsistencyValidator extends FileValidator { + private final GtfsRouteTableContainer routeTableContainer; + private final GtfsRouteNetworkTableContainer routeNetworkTableContainer; + + private final GtfsNetworkTableContainer networkTableContainer; + + @Inject + NetworkIdConsistencyValidator( + GtfsRouteTableContainer routes, + GtfsRouteNetworkTableContainer routeNetworks, + GtfsNetworkTableContainer networks) { + this.routeTableContainer = routes; + this.routeNetworkTableContainer = routeNetworks; + this.networkTableContainer = networks; + } + + @Override + public void validate(NoticeContainer noticeContainer) { + // Validate the presence of network_id in routes and its specification in either route_network + // or network files + boolean hasNetworkIdField = this.routeTableContainer.hasColumn(GtfsRoute.NETWORK_ID_FIELD_NAME); + if (hasNetworkIdField) { + if (!this.routeNetworkTableContainer.isMissingFile()) { + noticeContainer.addValidationNotice( + new RouteNetworksSpecifiedInMoreThanOneFileNotice( + GtfsRoute.FILENAME, GtfsRouteNetwork.FILENAME, GtfsRoute.NETWORK_ID_FIELD_NAME)); + } + if (!this.networkTableContainer.isMissingFile()) { + noticeContainer.addValidationNotice( + new RouteNetworksSpecifiedInMoreThanOneFileNotice( + GtfsRoute.FILENAME, GtfsNetwork.FILENAME, GtfsRoute.NETWORK_ID_FIELD_NAME)); + } + } + } + + /** + * Indicates that route network identifiers are specified across multiple files. + * + *

This notice highlights a data integrity issue where route network specifications are + * redundantly defined in more than one file. According to specifications, a route network + * identifier should be uniquely defined in a single file. Any additional definitions of route + * network specifications in other files are considered conditionally forbidden. + */ + @GtfsValidationNotice( + severity = ERROR, + files = + @GtfsValidationNotice.FileRefs({ + GtfsRouteSchema.class, + GtfsRouteNetworkSchema.class, + GtfsNetworkSchema.class + })) + static class RouteNetworksSpecifiedInMoreThanOneFileNotice extends ValidationNotice { + /** Name of the field in fileNameA */ + private final String fieldName; + + /** The name of the first file. */ + private final String fileNameA; + + /** The name of the second file which presence duplicates route networks specification. */ + private final String fileNameB; + + RouteNetworksSpecifiedInMoreThanOneFileNotice( + String filename1, String filename2, String fieldName) { + this.fileNameA = filename1; + this.fileNameB = filename2; + this.fieldName = fieldName; + } + } +} diff --git a/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/NetworkIdConsistencyValidatorTest.java b/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/NetworkIdConsistencyValidatorTest.java new file mode 100644 index 0000000000..0dfe65a6d2 --- /dev/null +++ b/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/NetworkIdConsistencyValidatorTest.java @@ -0,0 +1,86 @@ +package org.mobilitydata.gtfsvalidator.validator; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.ImmutableList; +import org.junit.Before; +import org.junit.Test; +import org.mobilitydata.gtfsvalidator.notice.NoticeContainer; +import org.mobilitydata.gtfsvalidator.parsing.CsvHeader; +import org.mobilitydata.gtfsvalidator.table.*; + +public class NetworkIdConsistencyValidatorTest { + + private NoticeContainer noticeContainer; + private GtfsRouteTableContainer routeTableContainer; + private GtfsRouteNetworkTableContainer routeNetworkTableContainer; + private GtfsNetworkTableContainer networkTableContainer; + + @Before + public void setup() { + noticeContainer = new NoticeContainer(); + routeTableContainer = + GtfsRouteTableContainer.forHeaderAndEntities( + new GtfsRouteTableDescriptor(), + new CsvHeader(ImmutableList.of("route_id", "network_id").toArray(new String[0])), + ImmutableList.of( + new GtfsRoute.Builder().setRouteId("123").setNetworkId("network1").build()), + noticeContainer); + routeNetworkTableContainer = + new GtfsRouteNetworkTableContainer( + new GtfsRouteNetworkTableDescriptor(), GtfsTableContainer.TableStatus.MISSING_FILE); + networkTableContainer = + new GtfsNetworkTableContainer( + new GtfsNetworkTableDescriptor(), GtfsTableContainer.TableStatus.MISSING_FILE); + } + + @Test + public void validatesConditionalForbiddenFilePresenceNoNotice() { + NetworkIdConsistencyValidator validator = + new NetworkIdConsistencyValidator( + routeTableContainer, routeNetworkTableContainer, networkTableContainer); + validator.validate(noticeContainer); + assertThat(noticeContainer.getValidationNotices().isEmpty()); + } + + @Test + public void validatesConditionalForbiddenFilePresence1() { + routeNetworkTableContainer = + GtfsRouteNetworkTableContainer.forEntities( + ImmutableList.of( + new GtfsRouteNetwork.Builder().setRouteId("123").setNetworkId("network1").build()), + noticeContainer); + NetworkIdConsistencyValidator validator = + new NetworkIdConsistencyValidator( + routeTableContainer, routeNetworkTableContainer, networkTableContainer); + validator.validate(noticeContainer); + assertThat(noticeContainer.getValidationNotices().size() == 1); + NetworkIdConsistencyValidator.RouteNetworksSpecifiedInMoreThanOneFileNotice notice = + (NetworkIdConsistencyValidator.RouteNetworksSpecifiedInMoreThanOneFileNotice) + noticeContainer.getValidationNotices().get(0); + NetworkIdConsistencyValidator.RouteNetworksSpecifiedInMoreThanOneFileNotice expectedNotice = + new NetworkIdConsistencyValidator.RouteNetworksSpecifiedInMoreThanOneFileNotice( + "routes.txt", "route_networks.txt", "network_id"); + assertThat(notice.toString().equals(expectedNotice.toString())); + } + + @Test + public void validatesConditionalForbiddenFilePresence2() { + networkTableContainer = + GtfsNetworkTableContainer.forEntities( + ImmutableList.of(new GtfsNetwork.Builder().setNetworkId("network1").build()), + noticeContainer); + NetworkIdConsistencyValidator validator = + new NetworkIdConsistencyValidator( + routeTableContainer, routeNetworkTableContainer, networkTableContainer); + validator.validate(noticeContainer); + assertThat(noticeContainer.getValidationNotices().size() == 1); + NetworkIdConsistencyValidator.RouteNetworksSpecifiedInMoreThanOneFileNotice notice = + (NetworkIdConsistencyValidator.RouteNetworksSpecifiedInMoreThanOneFileNotice) + noticeContainer.getValidationNotices().get(0); + NetworkIdConsistencyValidator.RouteNetworksSpecifiedInMoreThanOneFileNotice expectedNotice = + new NetworkIdConsistencyValidator.RouteNetworksSpecifiedInMoreThanOneFileNotice( + "routes.txt", "networks.txt", "network_id"); + assertThat(notice.toString().equals(expectedNotice.toString())); + } +} diff --git a/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/NoticeFieldsTest.java b/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/NoticeFieldsTest.java index 7877aa1d0f..6ed212c10e 100644 --- a/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/NoticeFieldsTest.java +++ b/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/NoticeFieldsTest.java @@ -193,7 +193,9 @@ public void testNoticeClassFieldNames() { "validator", "value", "maxShapeDistanceTraveled", - "maxTripDistanceTraveled"); + "maxTripDistanceTraveled", + "fileNameA", + "fileNameB"); } private static List discoverValidationNoticeFieldNames() {