├── .gitignore ├── LICENSE.txt ├── pom.xml └── src ├── main └── java │ └── org │ └── onebusaway │ └── gtfs_realtime │ └── exporter │ ├── GtfsRealtimeExporter.java │ ├── GtfsRealtimeExporterImpl.java │ ├── GtfsRealtimeExporterModule.java │ ├── GtfsRealtimeFileWriter.java │ ├── GtfsRealtimeFullUpdate.java │ ├── GtfsRealtimeGuiceBindingTypes.java │ ├── GtfsRealtimeIncrementalListener.java │ ├── GtfsRealtimeIncrementalUpdate.java │ ├── GtfsRealtimeLibrary.java │ ├── GtfsRealtimeServlet.java │ ├── GtfsRealtimeSink.java │ └── GtfsRealtimeSource.java ├── site ├── markdown │ ├── index.md.vm │ └── release-notes.md.vm └── site.xml └── test ├── java └── org │ └── onebusaway │ └── gtfs_realtime │ └── exporter │ ├── DeadlockTest.java │ ├── GtfsRealtimeExporterImplTest.java │ ├── GtfsRealtimeFileWriterTest.java │ └── IntegrationTest.java └── resources └── log4j.properties /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | release.properties 3 | pom.xml.releaseBackup 4 | .settings 5 | .project 6 | .classpath -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | COPYRIGHT_SECTION 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | onebusaway 5 | org.onebusaway 6 | 1.2.6 7 | 8 | onebusaway-gtfs-realtime-exporter 9 | 1.2.3-SNAPSHOT 10 | onebusaway-gtfs-realtime-exporter 11 | Support library for creating an application that shares GTFS-realtime data. 12 | https://github.com/OneBusAway/onebusaway-gtfs-realtime-exporter/wiki/ 13 | 14 | 15 | 1.2.21 16 | 1.0.2 17 | 9.0.5.v20130815 18 | 19 | 9900 20 | 21 | 22 | 23 | 24 | repo.camsys-apps.com 25 | https://repo.camsys-apps.com/third-party/ 26 | 27 | 28 | releases-camsys-public-repo 29 | https://repo.camsys-apps.com/releases/ 30 | 31 | true 32 | 33 | 34 | false 35 | 36 | 37 | 38 | snapshots-camsys-public-repo 39 | https://repo.camsys-apps.com/snapshots/ 40 | 41 | false 42 | 43 | 44 | true 45 | 46 | 47 | 48 | 49 | 50 | scm:git:http://github.com/OneBusAway/onebusaway-gtfs-realtime-exporter.git 51 | scm:git:https://github.com/OneBusAway/onebusaway-gtfs-realtime-exporter.git 52 | http://github.com/OneBusAway/onebusaway-gtfs-realtime-exporter 53 | 54 | 55 | 56 | GitHub 57 | https://github.com/OneBusAway/onebusaway-gtfs-realtime-exporter/issues 58 | 59 | 60 | 61 | 62 | 63 | ${site_id} 64 | ${site_url} 65 | 66 | 67 | 68 | 69 | 70 | org.onebusaway 71 | onebusaway-guice-jetty-exporter 72 | 1.2.0 73 | 74 | 75 | org.onebusaway 76 | onebusaway-gtfs-realtime-api 77 | ${gtfs_realtime_api_version} 78 | 79 | 80 | org.eclipse.jetty.websocket 81 | websocket-server 82 | ${jetty.version} 83 | 84 | 85 | org.slf4j 86 | slf4j-api 87 | 1.7.2 88 | 89 | 90 | 91 | junit 92 | junit 93 | 4.8.1 94 | test 95 | 96 | 97 | org.mockito 98 | mockito-core 99 | 1.8.0 100 | test 101 | 102 | 103 | org.slf4j 104 | slf4j-log4j12 105 | 1.7.2 106 | test 107 | 108 | 109 | org.onebusaway 110 | onebusaway-guice-jsr250 111 | ${onebusaway_guice_jsr250_version} 112 | test 113 | 114 | 115 | 116 | com.google.guava 117 | guava 118 | 19.0 119 | 120 | 121 | 122 | 123 | 124 | 125 | org.apache.maven.plugins 126 | maven-surefire-plugin 127 | 128 | 129 | ${org_onebusaway_test_port} 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /src/main/java/org/onebusaway/gtfs_realtime/exporter/GtfsRealtimeExporter.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013 Google, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.onebusaway.gtfs_realtime.exporter; 17 | 18 | import org.onebusaway.gtfs_realtime.exporter.GtfsRealtimeGuiceBindingTypes.TripUpdates; 19 | 20 | /** 21 | * Convenience interface that combines {@link GtfsRealtimeSource} and 22 | * {@link GtfsRealtimeSink} into one interface. 23 | * 24 | * @author bdferris 25 | */ 26 | public interface GtfsRealtimeExporter extends GtfsRealtimeSource, 27 | GtfsRealtimeSink { 28 | 29 | /** 30 | * A note on {@link AlertsExporter}, {@link TripUpdatesExporter}, 31 | * {@link VehiclePositionsExporter} and {@link MixedFeedExporter}: 32 | * 33 | * In order to support the use of {@link GtfsRealtimeGuiceBindingTypes} 34 | * annotations, where each annotation of the same time is backed by the same 35 | * singleton instance (eg. {@link TripUpdates}), it was necessary to create 36 | * sub-interfaces of {@link GtfsRealtimeExporter} for each of the 37 | * {@link GtfsRealtimeGuiceBindingTypes} such that singleton-scoped instances 38 | * could be bound to each of the sub-interfaces. Maybe someday Guice will 39 | * support this directly? 40 | * 41 | * See {@link GtfsRealtimeExporterModule} for more details. 42 | */ 43 | 44 | interface AlertsExporter extends GtfsRealtimeExporter { 45 | 46 | } 47 | 48 | interface TripUpdatesExporter extends GtfsRealtimeExporter { 49 | 50 | } 51 | 52 | interface VehiclePositionsExporter extends GtfsRealtimeExporter { 53 | 54 | } 55 | 56 | interface MixedFeedExporter extends GtfsRealtimeExporter { 57 | 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/org/onebusaway/gtfs_realtime/exporter/GtfsRealtimeExporterImpl.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013 Google, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.onebusaway.gtfs_realtime.exporter; 17 | 18 | import java.util.HashMap; 19 | import java.util.List; 20 | import java.util.Map; 21 | import java.util.concurrent.CopyOnWriteArrayList; 22 | import java.util.concurrent.TimeUnit; 23 | 24 | import com.google.common.cache.CacheBuilder; 25 | import com.google.inject.name.Named; 26 | import org.onebusaway.gtfs_realtime.exporter.GtfsRealtimeExporter.AlertsExporter; 27 | import org.onebusaway.gtfs_realtime.exporter.GtfsRealtimeExporter.MixedFeedExporter; 28 | import org.onebusaway.gtfs_realtime.exporter.GtfsRealtimeExporter.TripUpdatesExporter; 29 | import org.onebusaway.gtfs_realtime.exporter.GtfsRealtimeExporter.VehiclePositionsExporter; 30 | 31 | import com.google.transit.realtime.GtfsRealtime.FeedEntity; 32 | import com.google.transit.realtime.GtfsRealtime.FeedHeader; 33 | import com.google.transit.realtime.GtfsRealtime.FeedHeader.Incrementality; 34 | import com.google.transit.realtime.GtfsRealtime.FeedMessage; 35 | import com.google.transit.realtime.GtfsRealtimeConstants; 36 | import com.google.transit.realtime.GtfsRealtimeOneBusAway; 37 | import com.google.transit.realtime.GtfsRealtimeOneBusAway.OneBusAwayFeedHeader; 38 | 39 | import javax.inject.Inject; 40 | 41 | /** 42 | * Private implementation of {@link GtfsRealtimeExporter}. 43 | * 44 | * @author bdferris 45 | */ 46 | class GtfsRealtimeExporterImpl implements AlertsExporter, TripUpdatesExporter, 47 | VehiclePositionsExporter, MixedFeedExporter { 48 | 49 | private List _listeners = new CopyOnWriteArrayList(); 50 | 51 | private FeedHeader _header; 52 | 53 | private Map _feedEntities; 54 | 55 | private FeedMessage _cachedFeed = null; 56 | 57 | private long _incrementalIndex = 1; 58 | 59 | private int _incrementalHeartbeatInterval = 60; 60 | 61 | @Inject 62 | GtfsRealtimeExporterImpl(@Named("cache.expire.secs") String cacheExpireSecs) { 63 | this(Integer.parseInt(cacheExpireSecs)); 64 | } 65 | 66 | GtfsRealtimeExporterImpl() { 67 | this(0); 68 | } 69 | 70 | GtfsRealtimeExporterImpl(int cacheExpireSecs) { 71 | if (cacheExpireSecs > 0) { 72 | _feedEntities = CacheBuilder.newBuilder() 73 | .expireAfterWrite(cacheExpireSecs, TimeUnit.SECONDS) 74 | .build().asMap(); 75 | } 76 | else { 77 | _feedEntities = new HashMap(); 78 | } 79 | } 80 | 81 | /**** 82 | * {@link GtfsRealtimeSink} Interface 83 | ****/ 84 | 85 | @Override 86 | public synchronized void setFeedHeaderDefaults(FeedHeader header) { 87 | _header = header; 88 | _cachedFeed = null; 89 | } 90 | 91 | @Override 92 | public synchronized void handleFullUpdate(GtfsRealtimeFullUpdate update) { 93 | _cachedFeed = null; 94 | _feedEntities.clear(); 95 | for (FeedEntity entity : update.getEntities()) { 96 | _feedEntities.put(entity.getId(), entity); 97 | } 98 | _incrementalIndex++; 99 | FeedMessage feed = getFeed(); 100 | for (GtfsRealtimeIncrementalListener listener : _listeners) { 101 | listener.handleFeed(feed); 102 | } 103 | } 104 | 105 | @Override 106 | public synchronized void handleIncrementalUpdate( 107 | GtfsRealtimeIncrementalUpdate update) { 108 | _cachedFeed = null; 109 | 110 | for (FeedEntity toAdd : update.getUpdatedEntities()) { 111 | _feedEntities.put(toAdd.getId(), toAdd); 112 | } 113 | for (String toRemove : update.getDeletedEntities()) { 114 | _feedEntities.remove(toRemove); 115 | } 116 | 117 | FeedMessage.Builder feed = FeedMessage.newBuilder(); 118 | feed.setHeader(createIncrementalHeader()); 119 | feed.addAllEntity(update.getUpdatedEntities()); 120 | for (String toRemove : update.getDeletedEntities()) { 121 | FeedEntity.Builder entity = FeedEntity.newBuilder(); 122 | entity.setIsDeleted(true); 123 | entity.setId(toRemove); 124 | feed.addEntity(entity); 125 | } 126 | 127 | FeedMessage differentialFeed = feed.build(); 128 | for (GtfsRealtimeIncrementalListener listener : _listeners) { 129 | listener.handleFeed(differentialFeed); 130 | } 131 | _incrementalIndex++; 132 | } 133 | 134 | /**** 135 | * {@link GtfsRealtimeSource} Interface 136 | ****/ 137 | 138 | @Override 139 | public synchronized FeedMessage getFeed() { 140 | if (_cachedFeed == null) { 141 | FeedHeader.Builder header = FeedHeader.newBuilder(); 142 | if (_header != null) { 143 | header.mergeFrom(_header); 144 | } 145 | header.setIncrementality(Incrementality.FULL_DATASET); 146 | header.setTimestamp(System.currentTimeMillis() / 1000); 147 | header.setGtfsRealtimeVersion(GtfsRealtimeConstants.VERSION); 148 | 149 | setIncrementalIndex(header, _incrementalIndex - 1); 150 | 151 | FeedMessage.Builder feed = FeedMessage.newBuilder(); 152 | feed.setHeader(header); 153 | feed.addAllEntity(_feedEntities.values()); 154 | _cachedFeed = feed.build(); 155 | } 156 | return _cachedFeed; 157 | } 158 | 159 | @Override 160 | public void addIncrementalListener(GtfsRealtimeIncrementalListener listener) { 161 | _listeners.add(listener); 162 | listener.handleFeed(getFeed()); 163 | } 164 | 165 | @Override 166 | public void removeIncrementalListener(GtfsRealtimeIncrementalListener listener) { 167 | _listeners.remove(listener); 168 | } 169 | 170 | /*** 171 | * Private Methods 172 | ****/ 173 | 174 | private FeedHeader createIncrementalHeader() { 175 | FeedHeader.Builder header = FeedHeader.newBuilder(); 176 | if (_header != null) { 177 | header.mergeFrom(_header); 178 | } 179 | header.setIncrementality(Incrementality.DIFFERENTIAL); 180 | header.setTimestamp(System.currentTimeMillis() / 1000); 181 | header.setGtfsRealtimeVersion(GtfsRealtimeConstants.VERSION); 182 | 183 | setIncrementalIndex(header, _incrementalIndex); 184 | 185 | return header.build(); 186 | } 187 | 188 | private void setIncrementalIndex(FeedHeader.Builder header, 189 | long incrementalIndex) { 190 | OneBusAwayFeedHeader.Builder obaHeader = OneBusAwayFeedHeader.newBuilder(); 191 | obaHeader.setIncrementalIndex(incrementalIndex); 192 | obaHeader.setIncrementalHeartbeatInterval(_incrementalHeartbeatInterval); 193 | header.setExtension(GtfsRealtimeOneBusAway.obaFeedHeader, obaHeader.build()); 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/main/java/org/onebusaway/gtfs_realtime/exporter/GtfsRealtimeExporterModule.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2012 Google, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.onebusaway.gtfs_realtime.exporter; 17 | 18 | import java.util.Properties; 19 | import java.util.Set; 20 | import java.util.concurrent.Executors; 21 | import java.util.concurrent.ScheduledExecutorService; 22 | 23 | import org.onebusaway.gtfs_realtime.exporter.GtfsRealtimeExporter.AlertsExporter; 24 | import org.onebusaway.gtfs_realtime.exporter.GtfsRealtimeExporter.MixedFeedExporter; 25 | import org.onebusaway.gtfs_realtime.exporter.GtfsRealtimeExporter.TripUpdatesExporter; 26 | import org.onebusaway.gtfs_realtime.exporter.GtfsRealtimeExporter.VehiclePositionsExporter; 27 | import org.onebusaway.gtfs_realtime.exporter.GtfsRealtimeGuiceBindingTypes.Alerts; 28 | import org.onebusaway.gtfs_realtime.exporter.GtfsRealtimeGuiceBindingTypes.MixedFeed; 29 | import org.onebusaway.gtfs_realtime.exporter.GtfsRealtimeGuiceBindingTypes.TripUpdates; 30 | import org.onebusaway.gtfs_realtime.exporter.GtfsRealtimeGuiceBindingTypes.VehiclePositions; 31 | import org.onebusaway.guice.jetty_exporter.JettyExporterModule; 32 | 33 | import com.google.inject.AbstractModule; 34 | import com.google.inject.Module; 35 | import com.google.inject.Singleton; 36 | import com.google.inject.name.Names; 37 | 38 | /** 39 | * Provides Guice support for wiring up the services provided by the 40 | * onebusaway-gtfs-realtime-exporter library, along with all its dependencies. 41 | * 42 | * Here's a quick example of the library in use: 43 | * 44 | *
 45 |  * {@code
 46 |  * Set modules = new HashSet();
 47 |  * GtfsRealtimeExporterModule.addModuleAndDependencies(modules);
 48 |  * Injector injector = Guice.createInjector(modules);
 49 |  * }
 50 |  * 
51 | * 52 | * @author bdferris 53 | */ 54 | public class GtfsRealtimeExporterModule extends AbstractModule { 55 | 56 | public static final String NAME_EXECUTOR = "org.onebusway.gtfs_realtime.exporter.GtfsRealtimeExporterModule.executor"; 57 | 58 | /** 59 | * Adds a {@link GtfsRealtimeExporterModule} instance to the specified set of 60 | * modules, along with all its dependencies. 61 | * 62 | * @param modules the resulting set of Guice modules 63 | */ 64 | public static void addModuleAndDependencies(Set modules) { 65 | modules.add(new GtfsRealtimeExporterModule()); 66 | JettyExporterModule.addModuleAndDependencies(modules); 67 | } 68 | 69 | /** 70 | * See {@link GtfsRealtimeExporter} for a discussion of the somewhat 71 | * convoluted binding scheme used here. 72 | */ 73 | @Override 74 | protected void configure() { 75 | 76 | bind(GtfsRealtimeSink.class).annotatedWith(Alerts.class).to( 77 | AlertsExporter.class); 78 | bind(GtfsRealtimeSource.class).annotatedWith(Alerts.class).to( 79 | AlertsExporter.class); 80 | bind(GtfsRealtimeExporter.class).annotatedWith(Alerts.class).to( 81 | AlertsExporter.class); 82 | bind(AlertsExporter.class).to(GtfsRealtimeExporterImpl.class).in( 83 | Singleton.class); 84 | 85 | bind(GtfsRealtimeSink.class).annotatedWith(TripUpdates.class).to( 86 | TripUpdatesExporter.class); 87 | bind(GtfsRealtimeSource.class).annotatedWith(TripUpdates.class).to( 88 | TripUpdatesExporter.class); 89 | bind(GtfsRealtimeExporter.class).annotatedWith(TripUpdates.class).to( 90 | TripUpdatesExporter.class); 91 | bind(TripUpdatesExporter.class).to(GtfsRealtimeExporterImpl.class).in( 92 | Singleton.class); 93 | 94 | bind(GtfsRealtimeSink.class).annotatedWith(VehiclePositions.class).to( 95 | VehiclePositionsExporter.class); 96 | bind(GtfsRealtimeSource.class).annotatedWith(VehiclePositions.class).to( 97 | VehiclePositionsExporter.class); 98 | bind(GtfsRealtimeExporter.class).annotatedWith(VehiclePositions.class).to( 99 | VehiclePositionsExporter.class); 100 | bind(VehiclePositionsExporter.class).to(GtfsRealtimeExporterImpl.class).in( 101 | Singleton.class); 102 | 103 | bind(GtfsRealtimeSink.class).annotatedWith(MixedFeed.class).to( 104 | MixedFeedExporter.class); 105 | bind(GtfsRealtimeSource.class).annotatedWith(MixedFeed.class).to( 106 | MixedFeedExporter.class); 107 | bind(MixedFeedExporter.class).to(GtfsRealtimeExporterImpl.class).in( 108 | Singleton.class); 109 | 110 | bind(ScheduledExecutorService.class).annotatedWith( 111 | Names.named(NAME_EXECUTOR)).toInstance( 112 | Executors.newSingleThreadScheduledExecutor()); 113 | 114 | String expire = System.getProperty("cache.expire.secs", "0"); 115 | bindConstant().annotatedWith(Names.named("cache.expire.secs")).to(expire); 116 | } 117 | 118 | /** 119 | * Implement hashCode() and equals() such that two instances of the module 120 | * will be equal. 121 | */ 122 | @Override 123 | public int hashCode() { 124 | return this.getClass().hashCode(); 125 | } 126 | 127 | @Override 128 | public boolean equals(Object o) { 129 | if (this == o) 130 | return true; 131 | if (o == null) 132 | return false; 133 | return this.getClass().equals(o.getClass()); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/main/java/org/onebusaway/gtfs_realtime/exporter/GtfsRealtimeFileWriter.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2012 Google, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.onebusaway.gtfs_realtime.exporter; 17 | 18 | import java.io.BufferedOutputStream; 19 | import java.io.File; 20 | import java.io.FileOutputStream; 21 | import java.io.IOException; 22 | import java.io.OutputStream; 23 | import java.util.concurrent.ScheduledExecutorService; 24 | import java.util.concurrent.ScheduledFuture; 25 | import java.util.concurrent.TimeUnit; 26 | 27 | import javax.annotation.PostConstruct; 28 | import javax.annotation.PreDestroy; 29 | import javax.inject.Inject; 30 | import javax.inject.Named; 31 | 32 | import org.slf4j.Logger; 33 | import org.slf4j.LoggerFactory; 34 | 35 | import com.google.protobuf.Message; 36 | 37 | /** 38 | * Provides functionality for periodically writing a GTFS-realtime feed to an 39 | * output file. 40 | * 41 | * @author bdferris 42 | * 43 | */ 44 | public class GtfsRealtimeFileWriter { 45 | 46 | private static final Logger _log = LoggerFactory.getLogger(GtfsRealtimeFileWriter.class); 47 | 48 | protected GtfsRealtimeSource _source; 49 | 50 | private ScheduledExecutorService _executor; 51 | 52 | private File _path; 53 | 54 | private int _period = 5; 55 | 56 | private ScheduledFuture _task; 57 | 58 | public void setSource(GtfsRealtimeSource source) { 59 | _source = source; 60 | } 61 | 62 | @Inject 63 | public void setExecutor(@Named(GtfsRealtimeExporterModule.NAME_EXECUTOR) 64 | ScheduledExecutorService executor) { 65 | _executor = executor; 66 | } 67 | 68 | /** 69 | * @param path the output path where the feed will be written 70 | */ 71 | public void setPath(File path) { 72 | _path = path; 73 | } 74 | 75 | public int getPeriod() { 76 | return _period; 77 | } 78 | 79 | public void setPeriod(int timeInSeconds) { 80 | _period = timeInSeconds; 81 | } 82 | 83 | @PostConstruct 84 | public void start() { 85 | _task = _executor.scheduleAtFixedRate(new TaskEntryPoint(), 0, _period, 86 | TimeUnit.SECONDS); 87 | } 88 | 89 | @PreDestroy 90 | public void stop() { 91 | if (_task != null) { 92 | _task.cancel(false); 93 | _task = null; 94 | } 95 | } 96 | 97 | protected void writeMessageToFile() throws IOException { 98 | Message message = _source.getFeed(); 99 | OutputStream out = new BufferedOutputStream(new FileOutputStream(_path)); 100 | message.writeTo(out); 101 | out.close(); 102 | } 103 | 104 | private class TaskEntryPoint implements Runnable { 105 | 106 | @Override 107 | public void run() { 108 | try { 109 | writeMessageToFile(); 110 | } catch (IOException ex) { 111 | _log.error("Error writing message to output file: " + _path, ex); 112 | } 113 | } 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /src/main/java/org/onebusaway/gtfs_realtime/exporter/GtfsRealtimeFullUpdate.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013 Google, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.onebusaway.gtfs_realtime.exporter; 17 | 18 | import java.util.ArrayList; 19 | import java.util.List; 20 | 21 | import com.google.transit.realtime.GtfsRealtime.FeedEntity; 22 | 23 | /** 24 | * Specifies a full update to a GTFS-realtime feed, such that all previous 25 | * entities are replaced by the new entities contained in the update. 26 | * 27 | * @author bdferris 28 | * @see GtfsRealtimeIncrementalUpdate 29 | * @see GtfsRealtimeSink 30 | */ 31 | public class GtfsRealtimeFullUpdate { 32 | 33 | private List entities = new ArrayList(); 34 | 35 | public void addEntity(FeedEntity entity) { 36 | entities.add(entity); 37 | } 38 | 39 | public List getEntities() { 40 | return entities; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/org/onebusaway/gtfs_realtime/exporter/GtfsRealtimeGuiceBindingTypes.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013 Google, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.onebusaway.gtfs_realtime.exporter; 17 | 18 | import static java.lang.annotation.ElementType.FIELD; 19 | import static java.lang.annotation.ElementType.METHOD; 20 | import static java.lang.annotation.ElementType.PARAMETER; 21 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 22 | 23 | import java.lang.annotation.Retention; 24 | import java.lang.annotation.Target; 25 | 26 | import com.google.inject.BindingAnnotation; 27 | 28 | /** 29 | * We use Guice for wiring up dependices in the 30 | * onebusaway-gtfs-realtime-exporter library. For most clients, that means 31 | * injecting instances of {@link GtfsRealtimeSink}, {@link GtfsRealtimeSource}, 32 | * and {@link GtfsRealtimeExporter}. Typically, clients may produce different 33 | * combinations of GTFS-realtime feeds: alerts, trip-updates, and 34 | * vehicles-positions, each modeled with a separate feed. Each feed type needs 35 | * shared instances of {@link GtfsRealtimeSink} or {@link GtfsRealtimeSource} 36 | * specific to that feed type. 37 | * 38 | * To achieve this, we use Guice Binding Annotations to indicate which type of 39 | * feed we'd like. For example, for a {@link GtfsRealtimeSink} for trip-updates, 40 | * we'd use the following: 41 | * 42 | *
 43 |  * {@literal @}Inject
 44 |  * void setTripUpdatesSink({@literal @}TripUpdates GtfsRealtimeSink tripUpdatesSink) {
 45 |  *   ...
 46 |  * }
 47 |  * 
48 | * 49 | * Note the use of the {@link TripUpdates} annotation on the injection 50 | * parameter. Any instance of {@link GtfsRealtimeSink}, 51 | * {@link GtfsRealtimeSource}, or {@link GtfsRealtimeExporter} that is annotated 52 | * with the {@link TripUpdates} annotation will receive the same underlying 53 | * instance. We provide annotations for {@link Alerts}, {@link VehiclePositions} 54 | * , and finally a {@link MixedFeed} if you wish to mix different GTFS-realtime 55 | * feed entity types in the same feed. 56 | * 57 | * Note: one limitation of this approach is that it's tricky to exporter 58 | * multiple different feeds of the same type using the library (eg. two 59 | * trip-updates feeds). 60 | * 61 | * @author bdferris 62 | */ 63 | public class GtfsRealtimeGuiceBindingTypes { 64 | 65 | /** 66 | * Annotation to indicate that a wired instance of {@link GtfsRealtimeSink}, 67 | * {@link GtfsRealtimeSource}, or {@link GtfsRealtimeExporter} must support 68 | * trip updates data. 69 | * 70 | * @author bdferris 71 | * @see GtfsRealtimeGuiceBindingTypes 72 | */ 73 | @BindingAnnotation 74 | @Target({FIELD, PARAMETER, METHOD}) 75 | @Retention(RUNTIME) 76 | public @interface TripUpdates { 77 | } 78 | 79 | /** 80 | * Annotation to indicate that a wired instance of {@link GtfsRealtimeSink}, 81 | * {@link GtfsRealtimeSource}, or {@link GtfsRealtimeExporter} must support 82 | * vehicle positions data. 83 | * 84 | * @author bdferris 85 | * @see GtfsRealtimeGuiceBindingTypes 86 | */ 87 | @BindingAnnotation 88 | @Target({FIELD, PARAMETER, METHOD}) 89 | @Retention(RUNTIME) 90 | public @interface VehiclePositions { 91 | } 92 | 93 | /** 94 | * Annotation to indicate that a wired instance of {@link GtfsRealtimeSink}, 95 | * {@link GtfsRealtimeSource}, or {@link GtfsRealtimeExporter} must support 96 | * alerts data. 97 | * 98 | * @author bdferris 99 | * @see GtfsRealtimeGuiceBindingTypes 100 | */ 101 | @BindingAnnotation 102 | @Target({FIELD, PARAMETER, METHOD}) 103 | @Retention(RUNTIME) 104 | public @interface Alerts { 105 | } 106 | 107 | /** 108 | * Annotation to indicate that a wired instance of {@link GtfsRealtimeSink}, 109 | * {@link GtfsRealtimeSource}, or {@link GtfsRealtimeExporter} must support a 110 | * mixture of trip updates, vehicle positions, and alerts data. 111 | * 112 | * @author bdferris 113 | * @see GtfsRealtimeGuiceBindingTypes 114 | */ 115 | @BindingAnnotation 116 | @Target({FIELD, PARAMETER, METHOD}) 117 | @Retention(RUNTIME) 118 | public @interface MixedFeed { 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/main/java/org/onebusaway/gtfs_realtime/exporter/GtfsRealtimeIncrementalListener.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013 Google, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.onebusaway.gtfs_realtime.exporter; 17 | 18 | import com.google.transit.realtime.GtfsRealtime.FeedMessage; 19 | import com.google.transit.realtime.GtfsRealtime.FeedHeader.Incrementality; 20 | 21 | /** 22 | * Defines an interface for receiving incremental GTFS-realtime updates. 23 | * 24 | * @author bdferris 25 | * 26 | */ 27 | public interface GtfsRealtimeIncrementalListener { 28 | 29 | /** 30 | * Handle a feed update. Updates can either be full datasets (as indicated by 31 | * a {@link Incrementality#FULL_DATASET} value in the header) or incremental 32 | * updates to the most-recent full dataset (as indicated by a 33 | * {@link Incrementality#DIFFERENTIAL} value in the header). 34 | * 35 | * @param feed the feed update 36 | */ 37 | public void handleFeed(FeedMessage feed); 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/org/onebusaway/gtfs_realtime/exporter/GtfsRealtimeIncrementalUpdate.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013 Google, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.onebusaway.gtfs_realtime.exporter; 17 | 18 | import java.util.ArrayList; 19 | import java.util.List; 20 | 21 | import com.google.transit.realtime.GtfsRealtime.FeedEntity; 22 | 23 | /** 24 | * Specifies an incremental update to a GTFS-realtime feed, such that new feed 25 | * entities will selectively replace any previously updated feed entities. It is 26 | * important to use stable {@link FeedEntity#getId()} id values across updates 27 | * in order for incremental updates to function properly. 28 | * 29 | * @author bdferris 30 | * @see GtfsRealtimeFullUpdate 31 | * @see GtfsRealtimeSink 32 | */ 33 | public class GtfsRealtimeIncrementalUpdate { 34 | 35 | private List updatedEntities = new ArrayList(); 36 | 37 | private List deletedEntities = new ArrayList(); 38 | 39 | private long expirationTime = -1; 40 | 41 | public void addUpdatedEntity(FeedEntity entity) { 42 | updatedEntities.add(entity); 43 | } 44 | 45 | public List getUpdatedEntities() { 46 | return updatedEntities; 47 | } 48 | 49 | public void addDeletedEntity(String entityId) { 50 | deletedEntities.add(entityId); 51 | } 52 | 53 | public List getDeletedEntities() { 54 | return deletedEntities; 55 | } 56 | 57 | public long getExpirationTime() { 58 | return expirationTime; 59 | } 60 | 61 | public void setExpirationTime(long expirationTimeInMilliseconds) { 62 | this.expirationTime = expirationTimeInMilliseconds; 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/org/onebusaway/gtfs_realtime/exporter/GtfsRealtimeLibrary.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2012 Google, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.onebusaway.gtfs_realtime.exporter; 17 | 18 | import com.google.transit.realtime.GtfsRealtime.TranslatedString; 19 | import com.google.transit.realtime.GtfsRealtime.TranslatedString.Translation; 20 | import com.google.transit.realtime.GtfsRealtimeConstants; 21 | import com.google.transit.realtime.GtfsRealtime.FeedHeader; 22 | import com.google.transit.realtime.GtfsRealtime.FeedMessage; 23 | import com.google.transit.realtime.GtfsRealtime.FeedHeader.Incrementality; 24 | 25 | /** 26 | * Provides a number of convenience methods for working with GTFS-realtime data. 27 | * 28 | * @author bdferris 29 | * 30 | */ 31 | public class GtfsRealtimeLibrary { 32 | 33 | /** 34 | * Constructs a new {@link FeedMessage.Builder} that can be used to build a 35 | * new GTFS-realtime feed message. The {@link FeedHeader} will already be 36 | * filled in as a {@link Incrementality#FULL_DATASET} and the timestamp of the 37 | * feed will be set to NOW. This is the minimal requirement for an empty feed 38 | * so the feed could be returned 'as-is' at this point. 39 | * 40 | * 41 | * @return a new feed message builder with a header already populated 42 | */ 43 | public static FeedMessage.Builder createFeedMessageBuilder() { 44 | long now = System.currentTimeMillis(); 45 | FeedHeader.Builder header = FeedHeader.newBuilder(); 46 | header.setTimestamp(now / 1000); 47 | header.setIncrementality(Incrementality.FULL_DATASET); 48 | header.setGtfsRealtimeVersion(GtfsRealtimeConstants.VERSION); 49 | FeedMessage.Builder feedMessageBuilder = FeedMessage.newBuilder(); 50 | feedMessageBuilder.setHeader(header); 51 | return feedMessageBuilder; 52 | } 53 | 54 | /** 55 | * @param text the text to include in the translated string 56 | * @return a new {@link TranslatedString} with just a single translation in 57 | * the default language of the feed using the specified text. 58 | */ 59 | public static TranslatedString getTextAsTranslatedString(String text) { 60 | TranslatedString.Builder builder = TranslatedString.newBuilder(); 61 | Translation.Builder translation = Translation.newBuilder(); 62 | translation.setText(text); 63 | builder.addTranslation(translation); 64 | return builder.build(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/org/onebusaway/gtfs_realtime/exporter/GtfsRealtimeServlet.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2011 Google, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.onebusaway.gtfs_realtime.exporter; 17 | 18 | import java.io.ByteArrayOutputStream; 19 | import java.io.IOException; 20 | import java.net.URL; 21 | import java.nio.ByteBuffer; 22 | 23 | import javax.servlet.Servlet; 24 | import javax.servlet.ServletException; 25 | import javax.servlet.http.HttpServlet; 26 | import javax.servlet.http.HttpServletRequest; 27 | import javax.servlet.http.HttpServletResponse; 28 | 29 | import org.eclipse.jetty.websocket.api.RemoteEndpoint; 30 | import org.eclipse.jetty.websocket.api.Session; 31 | import org.eclipse.jetty.websocket.api.UpgradeRequest; 32 | import org.eclipse.jetty.websocket.api.UpgradeResponse; 33 | import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose; 34 | import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect; 35 | import org.eclipse.jetty.websocket.api.annotations.WebSocket; 36 | import org.eclipse.jetty.websocket.servlet.WebSocketCreator; 37 | import org.eclipse.jetty.websocket.servlet.WebSocketServlet; 38 | import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory; 39 | import org.onebusaway.guice.jetty_exporter.ServletSource; 40 | import org.slf4j.Logger; 41 | import org.slf4j.LoggerFactory; 42 | 43 | import com.google.protobuf.Message; 44 | import com.google.transit.realtime.GtfsRealtime.FeedMessage; 45 | 46 | /** 47 | * Provides functionality to export a GTFS-realtime feed via HTTP, with support 48 | * for both traditional HTTP GET requests and also incremental requests via 49 | * WebSockets. 50 | * 51 | * @author bdferris 52 | */ 53 | public class GtfsRealtimeServlet extends WebSocketServlet implements 54 | ServletSource { 55 | 56 | private static final long serialVersionUID = 1L; 57 | 58 | private static final String CONTENT_TYPE = "application/x-google-protobuf"; 59 | 60 | private static final Logger _log = LoggerFactory.getLogger(GtfsRealtimeServlet.class); 61 | 62 | protected GtfsRealtimeSource _source; 63 | 64 | private URL _url; 65 | 66 | public void setSource(GtfsRealtimeSource source) { 67 | _source = source; 68 | } 69 | 70 | public void setUrl(URL url) { 71 | _url = url; 72 | } 73 | 74 | /**** 75 | * {@link WebSocketServlet} Interface 76 | ****/ 77 | 78 | @Override 79 | public void configure(WebSocketServletFactory factory) { 80 | factory.setCreator(new WebsocketCreatorImpl()); 81 | } 82 | 83 | /**** 84 | * {@link HttpServlet} Interface 85 | ****/ 86 | 87 | @Override 88 | protected void doGet(HttpServletRequest req, HttpServletResponse resp) 89 | throws ServletException, IOException { 90 | boolean debug = req.getParameter("debug") != null; 91 | Message message = _source.getFeed(); 92 | if (debug) { 93 | resp.getWriter().print(message); 94 | } else { 95 | resp.setContentType(CONTENT_TYPE); 96 | message.writeTo(resp.getOutputStream()); 97 | } 98 | } 99 | 100 | /**** 101 | * {@link ServletSource} Interface 102 | ****/ 103 | 104 | @Override 105 | public URL getUrl() { 106 | return _url; 107 | } 108 | 109 | @Override 110 | public Servlet getServlet() { 111 | return this; 112 | } 113 | 114 | /**** 115 | * Protected Methods 116 | ****/ 117 | 118 | class WebsocketCreatorImpl implements WebSocketCreator { 119 | @Override 120 | public Object createWebSocket(UpgradeRequest req, UpgradeResponse resp) { 121 | return new DataWebSocket(); 122 | } 123 | 124 | } 125 | 126 | @WebSocket 127 | public class DataWebSocket implements GtfsRealtimeIncrementalListener { 128 | 129 | private Session _session; 130 | 131 | @OnWebSocketConnect 132 | public void onOpen(Session session) { 133 | _log.info("client connect"); 134 | _session = session; 135 | _source.addIncrementalListener(this); 136 | } 137 | 138 | @OnWebSocketClose 139 | public void onClose(Session session, int closeCode, String message) { 140 | _session = null; 141 | _source.removeIncrementalListener(this); 142 | } 143 | 144 | /**** 145 | * {@link GtfsRealtimeIncrementalListener} Interface 146 | ****/ 147 | 148 | @Override 149 | public void handleFeed(FeedMessage feed) { 150 | byte[] buffer = null; 151 | try { 152 | ByteArrayOutputStream out = new ByteArrayOutputStream(); 153 | feed.writeTo(out); 154 | buffer = out.toByteArray(); 155 | } catch (IOException ex) { 156 | throw new IllegalStateException(ex); 157 | } 158 | 159 | sendMessage(buffer); 160 | } 161 | 162 | private synchronized void sendMessage(byte[] buffer) { 163 | Session session = _session; // copy handle to remove synch issues 164 | if (session == null || !session.isOpen()) { 165 | return; 166 | } 167 | try { 168 | RemoteEndpoint remote = session.getRemote(); 169 | remote.sendBytes(ByteBuffer.wrap(buffer)); 170 | } catch (Exception ex) { 171 | // If anything goes wrong, we close the connection. 172 | _log.error("error sending message to remote WebSocket client", ex); 173 | try { 174 | // The @OnWebSocketClose event might have already been trigger during 175 | // our attempt to write, but if not, let's close the connection 176 | // ourselves. 177 | if (session != null) { 178 | // This should automatically trigger an @OnWebSocketClose event. 179 | session.close(); 180 | } 181 | } catch (IOException ex2) { 182 | _log.error("error closing remote WebSocket connection", ex2); 183 | } 184 | } 185 | } 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/main/java/org/onebusaway/gtfs_realtime/exporter/GtfsRealtimeSink.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013 Google, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.onebusaway.gtfs_realtime.exporter; 17 | 18 | import com.google.transit.realtime.GtfsRealtime.FeedEntity; 19 | import com.google.transit.realtime.GtfsRealtime.FeedHeader; 20 | 21 | public interface GtfsRealtimeSink { 22 | 23 | /** 24 | * The exporter library will automatically fill in necessary 25 | * {@link FeedHeader} fields when exporting a feed. However, you may wish to 26 | * specify some additional fields to be included by default (eg. extensions). 27 | * You may specify a partially constructed {@link FeedHeader} object here that 28 | * will be used as a basis for constructing the actual headers sent to 29 | * clients. 30 | * 31 | * @param header a partially constructed header 32 | */ 33 | public void setFeedHeaderDefaults(FeedHeader header); 34 | 35 | /** 36 | * Send a full dataset update. The feed entities in the update will replace 37 | * all previous updates received so far. 38 | * 39 | * @param update 40 | */ 41 | public void handleFullUpdate(GtfsRealtimeFullUpdate update); 42 | 43 | /** 44 | * Send an incremental update. The feed entities will selectively replace any 45 | * previously updated feed entities. It is important to use stable 46 | * {@link FeedEntity#getId()} id values across updates in order for 47 | * incremental updates to function properly. 48 | * 49 | * @param update 50 | */ 51 | public void handleIncrementalUpdate(GtfsRealtimeIncrementalUpdate update); 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/org/onebusaway/gtfs_realtime/exporter/GtfsRealtimeSource.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2012 Google, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.onebusaway.gtfs_realtime.exporter; 17 | 18 | import com.google.transit.realtime.GtfsRealtime.FeedMessage; 19 | 20 | /** 21 | * Provides a source of GTFS-realtime data, providing methods to receive a full 22 | * data set view of a feed, along with incremental updates as well. 23 | * 24 | * @author bdferris 25 | * @see GtfsRealtimeIncrementalListener 26 | */ 27 | public interface GtfsRealtimeSource { 28 | 29 | /** 30 | * @return the full-dataset view of a GTFS-realtime feed 31 | */ 32 | public FeedMessage getFeed(); 33 | 34 | /** 35 | * Register a new incremental GTFS-realtime listener. 36 | * 37 | * @param listener 38 | */ 39 | public void addIncrementalListener(GtfsRealtimeIncrementalListener listener); 40 | 41 | /** 42 | * Un-register a new incremental GTFS-realtime listener. 43 | * 44 | * @param listener 45 | */ 46 | public void removeIncrementalListener(GtfsRealtimeIncrementalListener listener); 47 | } 48 | -------------------------------------------------------------------------------- /src/site/markdown/index.md.vm: -------------------------------------------------------------------------------- 1 | # OneBusAway GTFS-realtime Exporter 2 | 3 | #set( $H = '#' ) ## When using velocity templating (a .vm file), the ## indicates a Velocity comment, which makes Markdown section headers tricky 4 | 5 | Project documentation for the `onebusaway-gtfs-realtime-exporter`, which provides a Java library for easily sharing 6 | [GTFS-realtime](https://developers.google.com/transit/gtfs-realtime) data via an embedded web-server or by writing the 7 | data to file. 8 | 9 | **Current Version:** ${currentVersion} 10 | 11 | $H$H Getting the Library 12 | 13 | If you are using Maven to manage your project, it's easy to add a dependency for the library. First, add the 14 | [OneBusAway Maven repository](https://github.com/OneBusAway/onebusaway/wiki/Maven-Repository) to your project's `pom.xml`. 15 | Then, add a dependency for the library itself. Note that we also add a dependency on `onebusaway-guice-jsr250`, which we 16 | use for starting / stopping services. 17 | 18 | ~~~ 19 | 20 | 21 | org.onebusaway 22 | onebusaway-gtfs-realtime-exporter 23 | ${currentVersion} 24 | 25 | 26 | org.onebusaway 27 | onebusaway-guice-jsr250 28 | ${onebusaway_guice_jsr250_version} 29 | 30 | 31 | ~~~ 32 | 33 | $H$H Using the Library 34 | 35 | We make heavy use of [Guice](https://code.google.com/p/google-guice/) for dependency injection and management. 36 | 37 | $H$H Example Code 38 | 39 | First, we introduce a `VehiclePositionsProducer`, which is responsible for actually generating GTFS-realtime data about 40 | transit vehicles. Typically, that data comes from some other, internal data stream. The producer asks that an instance 41 | of [GtfsRealtimeSink](./apidocs/org/onebusaway/gtfs_realtime/exporter/GtfsRealtimeSink.html) be injected into the class, 42 | annotating with [@VehiclePositions](./apidocs/org/onebusaway/gtfs_realtime/exporter/GtfsRealtimeGuiceBindingTypes.VehiclePositions.html) 43 | to indicate that the GTFS-realtime sink should be for vehicle position data. 44 | 45 | The producer spawns a thread that repeatedly generates incremental vehicle position updates. Specifically, the producer 46 | sends [GtfsRealtimeIncrementalUpdate](./apidocs/org/onebusaway/gtfs_realtime/exporter/GtfsRealtimeIncrementalUpdate.html) 47 | objects to the sink every time it wants to send a batch of incremental updates. If you'd rather not send incremental 48 | updates, but instead to perform full feed updates, see [GtfsRealtimeFullUpdate](./apidocs/org/onebusaway/gtfs_realtime/exporter/GtfsRealtimeFullUpdate.html) 49 | instead. 50 | 51 | ~~~ 52 | @Singleton 53 | public class VehiclePositionsProducer { 54 | 55 | private GtfsRealtimeSink _vehiclePositionsSink; 56 | 57 | @Inject 58 | public void setVehiclePositionsSink(@vehiclePositions GtfsRealtimeSink vehiclePositionsSink) { 59 | _vehiclePositionsSink = vehiclePositionsSink; 60 | } 61 | 62 | @PostConstruct 63 | public void start() { 64 | ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); 65 | executor.submit(new Runnable() { 66 | @Override 67 | public void run() { 68 | runLoop(); 69 | } 70 | }); 71 | } 72 | 73 | private void runLoop() { 74 | while(true) { 75 | GtfsRealtimeIncrementalUpdate update = new GtfsRealtimeIncrementalUpdate(); 76 | FeedEntity.Builder entity = FeedEntity.newBuilder(); 77 | entity.setId("v123"); 78 | VehiclePosition.Builder vehicle = VehiclePosition.newBuilder(); 79 | // ... fill in vehicle position data here ... 80 | entity.setVehicle(vehicle); 81 | update.addUpdatedEntity(entity.build()); 82 | _vehiclePositionsSink.handleIncrementalUpdate(update); 83 | } 84 | } 85 | } 86 | ~~~ 87 | 88 | Once we have our GTFS-realtime producer, we need to wire it up with other modules and start everything running. 89 | Here, we show the `GtfsRealtimeProducerDemo` class, which is responsible for configuring a Guice Injector 90 | with all the necessary dependencies, wiring up our GTFS-realtime. We then inject a couple of classe instances: 91 | 92 | * [GtfsRealtimeSource](./apidocs/org/onebusaway/gtfs_realtime/exporter/GtfsRealtimeSource.html) - a source of 93 | GTFS-realtime data that we'll wire up to various export options. Again, we annotate with [@VehiclePositions](./apidocs/org/onebusaway/gtfs_realtime/exporter/GtfsRealtimeGuiceBindingTypes.VehiclePositions.html) 94 | to indicate that we care about vehicle position data. 95 | * [VehiclePositionsProducer] - we inject an instance to make sure it gets created 96 | * LifecycleService - we use this to start up all services annotated with @PostConstruct 97 | 98 | Next, we wire up our GTFS-realtime data source to a [GtfsRealtimeServlet](./apidocs/org/onebusaway/gtfs_realtime/exporter/GtfsRealtimeServlet.html) 99 | in order to share the feed via HTTP. We also connect the source to a [GtfsRealtimeFileWriter](./apidocs/org/onebusaway/gtfs_realtime/exporter/GtfsRealtimeFileWriter.html) 100 | in order to periodically write the feed to an output file. Finally, we start everything up. 101 | 102 | ~~~ 103 | public class GtfsRealtimeProducerDemo { 104 | 105 | private GtfsRealtimeSource _vehiclePositionsSource; 106 | private LifecycleService _lifecycleService; 107 | 108 | @Inject 109 | public void setVehiclePositionsProducer(VehiclePositionsProducer producer) { 110 | // This is just here to make sure VehiclePositionsProducer gets instantiated. 111 | } 112 | 113 | @Inject 114 | public void setVehiclePositionsSource(@VehiclePositions GtfsRealtimeSource vehiclePositionsSource) { 115 | _vehiclePositionsSource = vehiclePositionsSource; 116 | } 117 | 118 | @Inject 119 | public void setLifecycleService(LifecycleService lifecycleService) { 120 | _lifecycleService = lifecycleService; 121 | } 122 | 123 | public void run() { 124 | Set modules = new HashSet(); 125 | GtfsRealtimeExporterModule.addModuleAndDependencies(modules); 126 | JSR250Module.addModuleAndDependencies(modules); 127 | Injector injector = Guice.createInjector(modules); 128 | injector.injectMembers(this); 129 | 130 | GtfsRealtimeServlet servlet = injector.getInstance(GtfsRealtimeServlet.class); 131 | servlet.setSource(_vehiclePositionsSource); 132 | servlet.setUrl(new URL("http://localhost:8080/trip-updates")); 133 | 134 | GtfsRealtimeFileWriter fileWriter = injector.getInstance(GtfsRealtimeFileWriter.class); 135 | fileWriter.setSource(_vehiclePositionsSource); 136 | fileWriter.setPath(new File("/tmp/trip-updates.pd")); 137 | 138 | _lifecycleService.start(); 139 | } 140 | } 141 | ~~~ 142 | -------------------------------------------------------------------------------- /src/site/markdown/release-notes.md.vm: -------------------------------------------------------------------------------- 1 | # Release Notes 2 | 3 | #set( $H = '#' ) ## When using velocity templating (a .vm file), the ## indicates a Velocity comment, which makes Markdown section headers tricky 4 | 5 | $H$H ${currentVersion} 6 | 7 | * Full Documentation: ${site_base_url}/onebusaway-gtfs-realtime-from-siri-cli/${currentVersion}/ 8 | 9 | $H$H 1.1.0 10 | 11 | * Add a listener method to GtfsRealtimeProvider for receiving notification of updates. Also provide a default 12 | implementation of that behavior. 13 | 14 | * Bump onebusaway-gtfs-realtime-api to 1.1.0. 15 | 16 | * Full Documentation: ${site_base_url}/onebusaway-gtfs-realtime-from-siri-cli/1.1.0/ 17 | 18 | $H$H 1.0.1 19 | 20 | * Fix a bug with the timestamp value in the GTFS-realtime FeedHeader. 21 | * Full Documentation: ${site_base_url}/onebusaway-gtfs-realtime-from-siri-cli/1.0.1/ 22 | 23 | $H$H 1.0.0 24 | 25 | * Initial release 26 | * Full Documentation: ${site_base_url}/onebusaway-gtfs-realtime-from-siri-cli/1.0.0/ -------------------------------------------------------------------------------- /src/site/site.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/test/java/org/onebusaway/gtfs_realtime/exporter/DeadlockTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013 Google, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.onebusaway.gtfs_realtime.exporter; 17 | 18 | import static org.junit.Assert.assertTrue; 19 | 20 | import java.net.MalformedURLException; 21 | import java.net.URI; 22 | import java.net.URISyntaxException; 23 | import java.net.URL; 24 | import java.util.HashSet; 25 | import java.util.Set; 26 | import java.util.concurrent.atomic.AtomicLong; 27 | 28 | import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; 29 | import org.eclipse.jetty.websocket.api.annotations.WebSocket; 30 | import org.eclipse.jetty.websocket.client.WebSocketClient; 31 | import org.junit.After; 32 | import org.junit.BeforeClass; 33 | import org.junit.Test; 34 | import org.onebusaway.gtfs_realtime.exporter.GtfsRealtimeGuiceBindingTypes.VehiclePositions; 35 | import org.onebusaway.guice.jsr250.JSR250Module; 36 | import org.onebusaway.guice.jsr250.LifecycleService; 37 | import org.slf4j.Logger; 38 | import org.slf4j.LoggerFactory; 39 | 40 | import com.google.inject.Guice; 41 | import com.google.inject.Inject; 42 | import com.google.inject.Injector; 43 | import com.google.inject.Module; 44 | import com.google.protobuf.ExtensionRegistry; 45 | import com.google.transit.realtime.GtfsRealtime.FeedEntity; 46 | import com.google.transit.realtime.GtfsRealtime.VehiclePosition; 47 | import com.google.transit.realtime.GtfsRealtimeExtensions; 48 | 49 | public class DeadlockTest { 50 | 51 | private static final Logger _log = LoggerFactory.getLogger(DeadlockTest.class); 52 | 53 | private static ExtensionRegistry _extensionRegistry = ExtensionRegistry.newInstance(); 54 | 55 | private GtfsRealtimeExporter _exporter; 56 | 57 | private LifecycleService _lifecycleService = null; 58 | 59 | @BeforeClass 60 | public static void beforeClass() { 61 | GtfsRealtimeExtensions.registerExtensions(_extensionRegistry); 62 | } 63 | 64 | @After 65 | public void after() { 66 | if (_lifecycleService != null) { 67 | _lifecycleService.stop(); 68 | } 69 | } 70 | 71 | @Inject 72 | public void setExporter(@VehiclePositions 73 | GtfsRealtimeExporter exporter) { 74 | _exporter = exporter; 75 | } 76 | 77 | @Inject 78 | public void setLifecycleService(LifecycleService lifecycleService) { 79 | _lifecycleService = lifecycleService; 80 | } 81 | 82 | @Test 83 | public void testIncrementalFeed() throws Exception { 84 | Set modules = new HashSet(); 85 | GtfsRealtimeExporterModule.addModuleAndDependencies(modules); 86 | JSR250Module.addModuleAndDependencies(modules); 87 | Injector injector = Guice.createInjector(modules); 88 | injector.injectMembers(this); 89 | 90 | GtfsRealtimeServlet servlet = injector.getInstance(GtfsRealtimeServlet.class); 91 | servlet.setSource(_exporter); 92 | servlet.setUrl(getUrl()); 93 | 94 | _lifecycleService.start(); 95 | 96 | Thread dataSourceThread = new Thread(new DataSource(_exporter)); 97 | dataSourceThread.start(); 98 | 99 | WebSocketClient client = new WebSocketClient(); 100 | DataSink sink = new DataSink(); 101 | 102 | for (int i = 0; i < 20; ++i) { 103 | _log.info("connect"); 104 | client.start(); 105 | client.connect(sink, getWebsocketUri()); 106 | for (int j = 0; j < 2; ++j) { 107 | long beforeCount = sink.getMessageCount(); 108 | Thread.sleep(1000); 109 | long afterCount = sink.getMessageCount(); 110 | _log.info("message count=" + (afterCount - beforeCount)); 111 | assertTrue(afterCount - beforeCount > 5); 112 | } 113 | _log.info("disconnect"); 114 | client.stop(); 115 | } 116 | } 117 | 118 | private static URL getUrl() { 119 | try { 120 | String port = System.getProperty("org_onebusaway_test_port", "8080"); 121 | return new URL("http://localhost:" + port + "/my-servlet"); 122 | } catch (MalformedURLException ex) { 123 | throw new IllegalStateException(ex); 124 | } 125 | } 126 | 127 | private static URI getWebsocketUri() { 128 | try { 129 | String port = System.getProperty("org_onebusaway_test_port", "8080"); 130 | return new URI("ws://localhost:" + port + "/my-servlet"); 131 | } catch (URISyntaxException ex) { 132 | throw new IllegalStateException(ex); 133 | } 134 | } 135 | 136 | /** 137 | * Sends incremental feed messages as fast as it can. 138 | */ 139 | private static class DataSource implements Runnable { 140 | 141 | private GtfsRealtimeSink _sink; 142 | 143 | public DataSource(GtfsRealtimeSink sink) { 144 | _sink = sink; 145 | } 146 | 147 | @Override 148 | public void run() { 149 | FeedEntity.Builder entity = FeedEntity.newBuilder(); 150 | entity.setId("tacos"); 151 | VehiclePosition.Builder position = VehiclePosition.newBuilder(); 152 | entity.setVehicle(position); 153 | final FeedEntity feedEntity = entity.build(); 154 | 155 | while (!Thread.interrupted()) { 156 | GtfsRealtimeIncrementalUpdate update = new GtfsRealtimeIncrementalUpdate(); 157 | update.addUpdatedEntity(feedEntity); 158 | _sink.handleIncrementalUpdate(update); 159 | } 160 | } 161 | } 162 | 163 | /** 164 | * Counts messages received from a GTFS-realtime incremental feed WebSocket. 165 | */ 166 | @WebSocket 167 | public static class DataSink { 168 | 169 | private AtomicLong _messageCount = new AtomicLong(); 170 | 171 | public long getMessageCount() { 172 | return _messageCount.get(); 173 | } 174 | 175 | @OnWebSocketMessage 176 | public void onMessage(byte[] data, int offset, int length) { 177 | _messageCount.incrementAndGet(); 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/test/java/org/onebusaway/gtfs_realtime/exporter/GtfsRealtimeExporterImplTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013 Google, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.onebusaway.gtfs_realtime.exporter; 17 | 18 | import static org.junit.Assert.*; 19 | 20 | import org.junit.Before; 21 | import org.junit.Test; 22 | 23 | import com.google.transit.realtime.GtfsRealtimeOneBusAway; 24 | import com.google.transit.realtime.GtfsRealtime.FeedEntity; 25 | import com.google.transit.realtime.GtfsRealtime.FeedMessage; 26 | import com.google.transit.realtime.GtfsRealtime.FeedHeader.Incrementality; 27 | import com.google.transit.realtime.GtfsRealtimeOneBusAway.OneBusAwayFeedHeader; 28 | 29 | public class GtfsRealtimeExporterImplTest { 30 | 31 | private GtfsRealtimeExporterImpl _exporter; 32 | 33 | private ListenerImpl _listener; 34 | 35 | @Before 36 | public void setup() { 37 | _exporter = new GtfsRealtimeExporterImpl(); 38 | _listener = new ListenerImpl(); 39 | } 40 | 41 | @Test 42 | public void testFullUpdate() { 43 | GtfsRealtimeFullUpdate update = new GtfsRealtimeFullUpdate(); 44 | FeedEntity.Builder feedEntity = FeedEntity.newBuilder(); 45 | feedEntity.setId("v123"); 46 | update.addEntity(feedEntity.build()); 47 | 48 | _exporter.handleFullUpdate(update); 49 | 50 | FeedMessage feed = _exporter.getFeed(); 51 | assertEquals(Incrementality.FULL_DATASET, 52 | feed.getHeader().getIncrementality()); 53 | OneBusAwayFeedHeader obaHeader = feed.getHeader().getExtension( 54 | GtfsRealtimeOneBusAway.obaFeedHeader); 55 | assertEquals(1, obaHeader.getIncrementalIndex()); 56 | assertEquals(1, feed.getEntityCount()); 57 | assertEquals("v123", feed.getEntity(0).getId()); 58 | 59 | _exporter.addIncrementalListener(_listener); 60 | assertSame(feed, _listener.getFeed()); 61 | 62 | update = new GtfsRealtimeFullUpdate(); 63 | feedEntity = FeedEntity.newBuilder(); 64 | feedEntity.setId("v456"); 65 | update.addEntity(feedEntity.build()); 66 | 67 | _exporter.handleFullUpdate(update); 68 | 69 | feed = _exporter.getFeed(); 70 | obaHeader = feed.getHeader().getExtension( 71 | GtfsRealtimeOneBusAway.obaFeedHeader); 72 | assertEquals(2, obaHeader.getIncrementalIndex()); 73 | assertEquals(1, feed.getEntityCount()); 74 | assertEquals("v456", feed.getEntity(0).getId()); 75 | assertSame(feed, _listener.getFeed()); 76 | } 77 | 78 | @Test 79 | public void testIncrementalUpdate() { 80 | { 81 | GtfsRealtimeIncrementalUpdate update = new GtfsRealtimeIncrementalUpdate(); 82 | FeedEntity.Builder feedEntity = FeedEntity.newBuilder(); 83 | feedEntity.setId("v123"); 84 | update.addUpdatedEntity(feedEntity.build()); 85 | 86 | _exporter.handleIncrementalUpdate(update); 87 | } 88 | 89 | { 90 | FeedMessage feed = _exporter.getFeed(); 91 | assertEquals(Incrementality.FULL_DATASET, 92 | feed.getHeader().getIncrementality()); 93 | OneBusAwayFeedHeader obaHeader = feed.getHeader().getExtension( 94 | GtfsRealtimeOneBusAway.obaFeedHeader); 95 | assertEquals(1, obaHeader.getIncrementalIndex()); 96 | assertEquals(1, feed.getEntityCount()); 97 | assertEquals("v123", feed.getEntity(0).getId()); 98 | } 99 | 100 | { 101 | GtfsRealtimeIncrementalUpdate update = new GtfsRealtimeIncrementalUpdate(); 102 | FeedEntity.Builder feedEntity = FeedEntity.newBuilder(); 103 | feedEntity.setId("v456"); 104 | update.addUpdatedEntity(feedEntity.build()); 105 | update.addDeletedEntity("v123"); 106 | 107 | _exporter.handleIncrementalUpdate(update); 108 | } 109 | 110 | { 111 | FeedMessage feed = _exporter.getFeed(); 112 | OneBusAwayFeedHeader obaHeader = feed.getHeader().getExtension( 113 | GtfsRealtimeOneBusAway.obaFeedHeader); 114 | assertEquals(2, obaHeader.getIncrementalIndex()); 115 | assertEquals(1, feed.getEntityCount()); 116 | assertEquals("v456", feed.getEntity(0).getId()); 117 | } 118 | 119 | _exporter.addIncrementalListener(_listener); 120 | 121 | assertSame(_exporter.getFeed(), _listener.getFeed()); 122 | 123 | { 124 | GtfsRealtimeIncrementalUpdate update = new GtfsRealtimeIncrementalUpdate(); 125 | FeedEntity.Builder feedEntity = FeedEntity.newBuilder(); 126 | feedEntity.setId("v789"); 127 | update.addUpdatedEntity(feedEntity.build()); 128 | update.addDeletedEntity("v456"); 129 | 130 | _exporter.handleIncrementalUpdate(update); 131 | } 132 | 133 | { 134 | FeedMessage feed = _listener.getFeed(); 135 | assertEquals(Incrementality.DIFFERENTIAL, 136 | feed.getHeader().getIncrementality()); 137 | OneBusAwayFeedHeader obaHeader = feed.getHeader().getExtension( 138 | GtfsRealtimeOneBusAway.obaFeedHeader); 139 | assertEquals(3, obaHeader.getIncrementalIndex()); 140 | assertEquals(2, feed.getEntityCount()); 141 | assertEquals("v789", feed.getEntity(0).getId()); 142 | assertEquals("v456", feed.getEntity(1).getId()); 143 | assertTrue(feed.getEntity(1).getIsDeleted()); 144 | } 145 | } 146 | 147 | private static class ListenerImpl implements GtfsRealtimeIncrementalListener { 148 | 149 | private FeedMessage _feed; 150 | 151 | public FeedMessage getFeed() { 152 | return _feed; 153 | } 154 | 155 | @Override 156 | public void handleFeed(FeedMessage feed) { 157 | _feed = feed; 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/test/java/org/onebusaway/gtfs_realtime/exporter/GtfsRealtimeFileWriterTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013 Google, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.onebusaway.gtfs_realtime.exporter; 17 | 18 | import static org.junit.Assert.assertEquals; 19 | import static org.junit.Assert.assertTrue; 20 | 21 | import java.io.File; 22 | import java.io.FileInputStream; 23 | import java.io.IOException; 24 | import java.io.InputStream; 25 | import java.util.concurrent.ScheduledExecutorService; 26 | import java.util.concurrent.TimeUnit; 27 | 28 | import org.junit.Before; 29 | import org.junit.Test; 30 | import org.mockito.ArgumentCaptor; 31 | import org.mockito.Mockito; 32 | 33 | import com.google.transit.realtime.GtfsRealtime.FeedHeader; 34 | import com.google.transit.realtime.GtfsRealtime.FeedHeader.Incrementality; 35 | import com.google.transit.realtime.GtfsRealtime.FeedMessage; 36 | import com.google.transit.realtime.GtfsRealtimeConstants; 37 | 38 | public class GtfsRealtimeFileWriterTest { 39 | 40 | private GtfsRealtimeFileWriter _writer; 41 | 42 | private GtfsRealtimeSource _source; 43 | 44 | private ScheduledExecutorService _executor; 45 | 46 | private File _path; 47 | 48 | @Before 49 | public void setup() throws IOException { 50 | _writer = new GtfsRealtimeFileWriter(); 51 | 52 | _source = Mockito.mock(GtfsRealtimeSource.class); 53 | _writer.setSource(_source); 54 | 55 | _executor = Mockito.mock(ScheduledExecutorService.class); 56 | _writer.setExecutor(_executor); 57 | 58 | _path = File.createTempFile(GtfsRealtimeFileWriterTest.class.getName(), 59 | "-FeedMessage.pb"); 60 | _path.delete(); 61 | _path.deleteOnExit(); 62 | _writer.setPath(_path); 63 | } 64 | 65 | @Test 66 | public void test() throws IOException { 67 | ArgumentCaptor captureRunnable = ArgumentCaptor.forClass(Runnable.class); 68 | Mockito.when( 69 | _executor.scheduleAtFixedRate(captureRunnable.capture(), 70 | Mockito.eq(0L), Mockito.eq(5L), Mockito.eq(TimeUnit.SECONDS))).thenReturn( 71 | null); 72 | _writer.start(); 73 | 74 | FeedMessage.Builder feed = FeedMessage.newBuilder(); 75 | FeedHeader.Builder header = feed.getHeaderBuilder(); 76 | header.setIncrementality(Incrementality.FULL_DATASET); 77 | header.setTimestamp(1234L); 78 | header.setGtfsRealtimeVersion(GtfsRealtimeConstants.VERSION); 79 | 80 | Mockito.when(_source.getFeed()).thenReturn(feed.build()); 81 | 82 | Runnable writerTask = captureRunnable.getValue(); 83 | writerTask.run(); 84 | 85 | assertTrue(_path.exists()); 86 | 87 | InputStream in = new FileInputStream(_path); 88 | FeedMessage actualFeed = FeedMessage.parseFrom(in); 89 | assertEquals(header.getTimestamp(), actualFeed.getHeader().getTimestamp()); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/test/java/org/onebusaway/gtfs_realtime/exporter/IntegrationTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013 Google, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.onebusaway.gtfs_realtime.exporter; 17 | 18 | import static org.junit.Assert.assertEquals; 19 | 20 | import java.io.File; 21 | import java.io.FileInputStream; 22 | import java.io.IOException; 23 | import java.net.MalformedURLException; 24 | import java.net.URI; 25 | import java.net.URISyntaxException; 26 | import java.net.URL; 27 | import java.util.ArrayList; 28 | import java.util.HashSet; 29 | import java.util.List; 30 | import java.util.Set; 31 | import java.util.concurrent.CountDownLatch; 32 | import java.util.concurrent.Future; 33 | import java.util.concurrent.TimeUnit; 34 | 35 | import org.eclipse.jetty.websocket.api.Session; 36 | import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; 37 | import org.eclipse.jetty.websocket.api.annotations.WebSocket; 38 | import org.eclipse.jetty.websocket.client.WebSocketClient; 39 | import org.junit.After; 40 | import org.junit.BeforeClass; 41 | import org.junit.Test; 42 | import org.onebusaway.gtfs_realtime.exporter.GtfsRealtimeGuiceBindingTypes.VehiclePositions; 43 | import org.onebusaway.guice.jsr250.JSR250Module; 44 | import org.onebusaway.guice.jsr250.LifecycleService; 45 | 46 | import com.google.inject.Guice; 47 | import com.google.inject.Inject; 48 | import com.google.inject.Injector; 49 | import com.google.inject.Module; 50 | import com.google.protobuf.ByteString; 51 | import com.google.protobuf.ExtensionRegistry; 52 | import com.google.protobuf.InvalidProtocolBufferException; 53 | import com.google.transit.realtime.GtfsRealtime.FeedEntity; 54 | import com.google.transit.realtime.GtfsRealtime.FeedHeader; 55 | import com.google.transit.realtime.GtfsRealtime.FeedHeader.Incrementality; 56 | import com.google.transit.realtime.GtfsRealtime.FeedMessage; 57 | import com.google.transit.realtime.GtfsRealtime.VehiclePosition; 58 | import com.google.transit.realtime.GtfsRealtimeExtensions; 59 | import com.google.transit.realtime.GtfsRealtimeOneBusAway; 60 | import com.google.transit.realtime.GtfsRealtimeOneBusAway.OneBusAwayFeedHeader; 61 | 62 | public class IntegrationTest { 63 | 64 | private static ExtensionRegistry _extensionRegistry = ExtensionRegistry.newInstance(); 65 | 66 | private GtfsRealtimeExporter _exporter; 67 | 68 | private LifecycleService _lifecycleService = null; 69 | 70 | @BeforeClass 71 | public static void beforeClass() { 72 | GtfsRealtimeExtensions.registerExtensions(_extensionRegistry); 73 | } 74 | 75 | @After 76 | public void after() { 77 | if (_lifecycleService != null) { 78 | _lifecycleService.stop(); 79 | } 80 | } 81 | 82 | @Inject 83 | public void setExporter(@VehiclePositions 84 | GtfsRealtimeExporter exporter) { 85 | _exporter = exporter; 86 | } 87 | 88 | @Inject 89 | public void setLifecycleService(LifecycleService lifecycleService) { 90 | _lifecycleService = lifecycleService; 91 | } 92 | 93 | @Test 94 | public void testFullFeed() throws IOException, InterruptedException { 95 | URL url = getUrl(); 96 | 97 | File path = File.createTempFile(getClass().getName() + "-", ".pb"); 98 | path.delete(); 99 | path.deleteOnExit(); 100 | 101 | Set modules = new HashSet(); 102 | GtfsRealtimeExporterModule.addModuleAndDependencies(modules); 103 | JSR250Module.addModuleAndDependencies(modules); 104 | Injector injector = Guice.createInjector(modules); 105 | injector.injectMembers(this); 106 | 107 | GtfsRealtimeServlet servlet = injector.getInstance(GtfsRealtimeServlet.class); 108 | servlet.setSource(_exporter); 109 | servlet.setUrl(url); 110 | 111 | GtfsRealtimeFileWriter fileWriter = injector.getInstance(GtfsRealtimeFileWriter.class); 112 | fileWriter.setSource(_exporter); 113 | fileWriter.setPath(path); 114 | 115 | _lifecycleService.start(); 116 | 117 | { 118 | GtfsRealtimeIncrementalUpdate update = new GtfsRealtimeIncrementalUpdate(); 119 | FeedEntity.Builder entity = FeedEntity.newBuilder(); 120 | entity.setId("tacos"); 121 | VehiclePosition.Builder position = VehiclePosition.newBuilder(); 122 | entity.setVehicle(position); 123 | update.addUpdatedEntity(entity.build()); 124 | _exporter.handleIncrementalUpdate(update); 125 | } 126 | 127 | Thread.sleep((fileWriter.getPeriod() + 1) * 1000); 128 | 129 | FeedMessage feed = FeedMessage.parseFrom(new FileInputStream(path)); 130 | assertEquals(1, feed.getEntityCount()); 131 | assertEquals("tacos", feed.getEntity(0).getId()); 132 | 133 | feed = FeedMessage.parseFrom(url.openStream()); 134 | assertEquals(1, feed.getEntityCount()); 135 | assertEquals("tacos", feed.getEntity(0).getId()); 136 | 137 | { 138 | GtfsRealtimeIncrementalUpdate update = new GtfsRealtimeIncrementalUpdate(); 139 | FeedEntity.Builder entity = FeedEntity.newBuilder(); 140 | entity.setId("nachos"); 141 | VehiclePosition.Builder position = VehiclePosition.newBuilder(); 142 | entity.setVehicle(position); 143 | update.addUpdatedEntity(entity.build()); 144 | _exporter.handleIncrementalUpdate(update); 145 | } 146 | 147 | Thread.sleep((fileWriter.getPeriod() + 1) * 1000); 148 | 149 | feed = FeedMessage.parseFrom(new FileInputStream(path)); 150 | assertEquals(2, feed.getEntityCount()); 151 | 152 | feed = FeedMessage.parseFrom(url.openStream()); 153 | assertEquals(2, feed.getEntityCount()); 154 | } 155 | 156 | @Test 157 | public void testIncrementalFeed() throws Exception { 158 | Set modules = new HashSet(); 159 | GtfsRealtimeExporterModule.addModuleAndDependencies(modules); 160 | JSR250Module.addModuleAndDependencies(modules); 161 | Injector injector = Guice.createInjector(modules); 162 | injector.injectMembers(this); 163 | 164 | GtfsRealtimeServlet servlet = injector.getInstance(GtfsRealtimeServlet.class); 165 | servlet.setSource(_exporter); 166 | servlet.setUrl(getUrl()); 167 | 168 | _lifecycleService.start(); 169 | 170 | { 171 | GtfsRealtimeIncrementalUpdate update = new GtfsRealtimeIncrementalUpdate(); 172 | FeedEntity.Builder entity = FeedEntity.newBuilder(); 173 | entity.setId("tacos"); 174 | VehiclePosition.Builder position = VehiclePosition.newBuilder(); 175 | entity.setVehicle(position); 176 | update.addUpdatedEntity(entity.build()); 177 | _exporter.handleIncrementalUpdate(update); 178 | } 179 | 180 | WebSocketClient client = new WebSocketClient(); 181 | client.start(); 182 | GtfsRealtimeSocket socket = new GtfsRealtimeSocket(); 183 | Future future = client.connect(socket, getWebsocketUri()); 184 | future.get(10, TimeUnit.SECONDS); 185 | 186 | CountDownLatch latch = socket.setLatch(1); 187 | latch.await(2, TimeUnit.SECONDS); 188 | 189 | List feeds = socket.getFeeds(); 190 | assertEquals(1, feeds.size()); 191 | FeedMessage feed = feeds.get(0); 192 | FeedHeader header = feed.getHeader(); 193 | assertEquals(Incrementality.FULL_DATASET, header.getIncrementality()); 194 | OneBusAwayFeedHeader obaHeader = header.getExtension(GtfsRealtimeOneBusAway.obaFeedHeader); 195 | long index = obaHeader.getIncrementalIndex(); 196 | assertEquals(1, index); 197 | assertEquals(1, feed.getEntityCount()); 198 | 199 | latch = socket.setLatch(1); 200 | 201 | { 202 | GtfsRealtimeIncrementalUpdate update = new GtfsRealtimeIncrementalUpdate(); 203 | FeedEntity.Builder entity = FeedEntity.newBuilder(); 204 | entity.setId("nachos"); 205 | VehiclePosition.Builder position = VehiclePosition.newBuilder(); 206 | entity.setVehicle(position); 207 | update.addUpdatedEntity(entity.build()); 208 | _exporter.handleIncrementalUpdate(update); 209 | } 210 | 211 | latch.await(); 212 | 213 | assertEquals(2, feeds.size()); 214 | feed = feeds.get(1); 215 | header = feed.getHeader(); 216 | assertEquals(Incrementality.DIFFERENTIAL, header.getIncrementality()); 217 | obaHeader = header.getExtension(GtfsRealtimeOneBusAway.obaFeedHeader); 218 | assertEquals(2, obaHeader.getIncrementalIndex()); 219 | assertEquals(1, feed.getEntityCount()); 220 | } 221 | 222 | private static URL getUrl() { 223 | try { 224 | String port = System.getProperty("org_onebusaway_test_port", "8080"); 225 | return new URL("http://localhost:" + port + "/my-servlet"); 226 | } catch (MalformedURLException ex) { 227 | throw new IllegalStateException(ex); 228 | } 229 | } 230 | 231 | private static URI getWebsocketUri() { 232 | try { 233 | String port = System.getProperty("org_onebusaway_test_port", "8080"); 234 | return new URI("ws://localhost:" + port + "/my-servlet"); 235 | } catch (URISyntaxException ex) { 236 | throw new IllegalStateException(ex); 237 | } 238 | } 239 | 240 | @WebSocket 241 | public static class GtfsRealtimeSocket { 242 | 243 | private CountDownLatch _latch = null; 244 | 245 | private List _feeds = new ArrayList(); 246 | 247 | public List getFeeds() { 248 | return _feeds; 249 | } 250 | 251 | public CountDownLatch setLatch(int count) { 252 | _latch = new CountDownLatch(count); 253 | return _latch; 254 | } 255 | 256 | @OnWebSocketMessage 257 | public void onMessage(byte[] data, int offset, int length) { 258 | try { 259 | FeedMessage feed = FeedMessage.parseFrom( 260 | ByteString.copyFrom(data, offset, length), _extensionRegistry); 261 | _feeds.add(feed); 262 | if (_latch != null) { 263 | _latch.countDown(); 264 | } 265 | } catch (InvalidProtocolBufferException ex) { 266 | throw new IllegalStateException(ex); 267 | } 268 | } 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /src/test/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | log4j.rootLogger = INFO, stderr 2 | 3 | log4j.appender.stderr = org.apache.log4j.ConsoleAppender 4 | # Follow must be set to true, so that when stderr is closed and reopened in daemonization, we'll continue to log 5 | log4j.appender.stderr.Follow = TRUE 6 | log4j.appender.stderr.Threshold = DEBUG 7 | log4j.appender.stderr.Target = System.err 8 | log4j.appender.stderr.layout = org.apache.log4j.PatternLayout 9 | log4j.appender.stderr.layout.ConversionPattern = %d{ISO8601} %-5p [%F:%L] : %m%n --------------------------------------------------------------------------------