package com.openxc; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import android.util.Log; import com.google.common.base.MoreObjects; import com.openxc.messages.KeyedMessage; import com.openxc.messages.MessageKey; import com.openxc.messages.VehicleMessage; import com.openxc.sinks.DataSinkException; import com.openxc.sinks.VehicleDataSink; import com.openxc.sources.SourceCallback; import com.openxc.sources.VehicleDataSource; /** * A pipeline that ferries data from VehicleDataSources to VehicleDataSinks. * * A DataPipeline accepts two types of components - sources and sinks. The * sources (implementing {@link VehicleDataSource} call the * {@link #receive(VehicleMessage)} method on the this class when new * values arrive. The DataPipeline then passes this value on to all currently * registered data sinks. * * The Pipeline can have an optional Operator, which implements a few callbacks * to check the status of the pipeline - e.g. if some source in the pipeline is * active. */ public class DataPipeline implements SourceCallback { private static final String TAG = "DataPipeline"; private Operator mOperator; private int mMessagesReceived = 0; private Map<MessageKey, KeyedMessage> mKeyedMessages = new ConcurrentHashMap<>(); private CopyOnWriteArrayList<VehicleDataSink> mSinks = new CopyOnWriteArrayList<>(); private CopyOnWriteArrayList<VehicleDataSource> mSources = new CopyOnWriteArrayList<>(); public interface Operator { public void onPipelineDeactivated(); public void onPipelineActivated(); } public DataPipeline() { this(null); } public DataPipeline(Operator operator) { mOperator = operator; } /** * Accept new values from data sources and send it out to all registered * sinks. * * This method is required to implement the SourceCallback interface. * * If any data sink throws a DataSinkException when receiving data, it will * be removed from the list of sinks. */ @Override public void receive(VehicleMessage message) { if(message == null) { return; } if(message instanceof KeyedMessage) { KeyedMessage keyedMessage = message.asKeyedMessage(); mKeyedMessages.put(keyedMessage.getKey(), keyedMessage); } List<VehicleDataSink> deadSinks = new ArrayList<>(); for (VehicleDataSink sink : mSinks) { try { sink.receive(message); } catch (DataSinkException e) { Log.w(TAG, this.getClass().getName() + ": The sink " + sink + " exploded when we sent a new message " + "-- removing it from the pipeline: " + e); deadSinks.add(sink); } } mMessagesReceived++; for(VehicleDataSink sink : deadSinks) { removeSink(sink); } } /** * Add a new sink to the pipeline. */ public VehicleDataSink addSink(VehicleDataSink sink) { mSinks.add(sink); return sink; } /** * Remove a previously added sink from the pipeline. * * Once removed, the sink will no longer receive any new messages from * the pipeline's sources. The sink's {@link VehicleDataSink#stop()} method * is also called. * * @param sink if the value is null, it is ignored. */ public void removeSink(VehicleDataSink sink) { if(sink != null) { mSinks.remove(sink); sink.stop(); } } /** * Add a new source to the pipeline. * * The source is given a reference to this DataPipeline as its callback. */ public VehicleDataSource addSource(VehicleDataSource source) { source.setCallback(this); mSources.add(source); if(isActive()) { source.onPipelineActivated(); } else { source.onPipelineDeactivated(); } return source; } /** * Remove a previously added source from the pipeline. * * Once removed, the source should no longer use this pipeline for its * callback. * * @param source if the value is null, it is ignored. */ public void removeSource(VehicleDataSource source) { if(source != null) { mSources.remove(source); source.stop(); } } public List<VehicleDataSource> getSources() { return mSources; } public List<VehicleDataSink> getSinks() { return mSinks; } /** * Clear all sources and sinks from the pipeline and stop all of them. */ public void stop() { clearSources(); clearSinks(); } /** * Remove and stop all sources in the pipeline. */ public void clearSources() { for (VehicleDataSource mSource : mSources) { (mSource).stop(); } mSources.clear(); } /** * Remove and stop all sinks in the pipeline. */ public void clearSinks() { for (VehicleDataSink mSink : mSinks) { (mSink).stop(); } mSinks.clear(); } /** * Return the last received value for the keyed message if known. * * @param key the key of the message to retrieve, if any available. * @return a VehicleMessage with the last known value, or null if no value * has been received. */ public KeyedMessage get(MessageKey key) { return mKeyedMessages.get(key); } /** * @return number of messages received since instantiation. */ public int getMessageCount() { return mMessagesReceived; } public boolean isActive() { return isActive(null); } /** * Return true if at least one source is active. * * @param skipSource don't consider this data source when determining if the * pipeline is active. */ public boolean isActive(VehicleDataSource skipSource) { boolean connected = false; for(VehicleDataSource s : mSources) { if(s != skipSource) { connected = connected || s.isConnected(); } } return connected; } /** * At least one source is not active - if all sources are inactive, notify * the operator. */ @Override public void sourceDisconnected(VehicleDataSource source) { if(mOperator != null) { if(!isActive(source)) { mOperator.onPipelineDeactivated(); for(VehicleDataSource s : mSources) { s.onPipelineDeactivated(); } } } } /** * At least one source is active - notify the operator. */ @Override public void sourceConnected(VehicleDataSource source) { if(mOperator != null) { mOperator.onPipelineActivated(); for(VehicleDataSource s : mSources) { s.onPipelineActivated(); } } } @Override public String toString() { return MoreObjects.toStringHelper(this) .add("sources", mSources) .add("sinks", mSinks) .add("numKeyedMessageTypes", mKeyedMessages.size()) .toString(); } }