/*
* Copyright (C) 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package org.ros.internal.node.topic;
import com.google.common.base.Preconditions;
import org.apache.commons.logging.Log;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.channel.Channel;
import org.ros.concurrent.ListenerGroup;
import org.ros.concurrent.SignalRunnable;
import org.ros.internal.node.server.NodeIdentifier;
import org.ros.internal.transport.ConnectionHeader;
import org.ros.internal.transport.ConnectionHeaderFields;
import org.ros.internal.transport.queue.OutgoingMessageQueue;
import org.ros.log.RosLogFactory;
import org.ros.message.MessageFactory;
import org.ros.message.MessageSerializer;
import org.ros.node.topic.DefaultPublisherListener;
import org.ros.node.topic.Publisher;
import org.ros.node.topic.PublisherListener;
import org.ros.node.topic.Subscriber;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* Default implementation of a {@link Publisher}.
*
* @author damonkohler@google.com (Damon Kohler)
*/
public class DefaultPublisher<T> extends DefaultTopicParticipant implements Publisher<T> {
private static final Log log = RosLogFactory.getLog(DefaultPublisher.class);
/**
* The maximum delay before shutdown will begin even if all
* {@link PublisherListener}s have not yet returned from their
* {@link PublisherListener#onShutdown(Publisher)} callback.
*/
private static final long DEFAULT_SHUTDOWN_TIMEOUT = 5;
private static final TimeUnit DEFAULT_SHUTDOWN_TIMEOUT_UNITS = TimeUnit.SECONDS;
/**
* Queue of all messages being published by this {@link Publisher}.
*/
private final OutgoingMessageQueue<T> outgoingMessageQueue;
private final ListenerGroup<PublisherListener<T>> listeners;
private final NodeIdentifier nodeIdentifier;
private final MessageFactory messageFactory;
public DefaultPublisher(NodeIdentifier nodeIdentifier, TopicDeclaration topicDeclaration,
MessageSerializer<T> serializer, MessageFactory messageFactory, ScheduledExecutorService executorService) {
super(topicDeclaration);
this.nodeIdentifier = nodeIdentifier;
this.messageFactory = messageFactory;
outgoingMessageQueue = new OutgoingMessageQueue<T>(serializer, executorService);
listeners = new ListenerGroup<PublisherListener<T>>(executorService);
listeners.add(new DefaultPublisherListener<T>() {
@Override
public void onMasterRegistrationSuccess(Publisher<T> registrant) {
log.info("Publisher registered: " + DefaultPublisher.this);
}
@Override
public void onMasterRegistrationFailure(Publisher<T> registrant) {
log.info("Publisher registration failed: " + DefaultPublisher.this);
}
@Override
public void onMasterUnregistrationSuccess(Publisher<T> registrant) {
log.info("Publisher unregistered: " + DefaultPublisher.this);
}
@Override
public void onMasterUnregistrationFailure(Publisher<T> registrant) {
log.info("Publisher unregistration failed: " + DefaultPublisher.this);
}
});
}
@Override
public void setLatchMode(boolean enabled) {
outgoingMessageQueue.setLatchMode(enabled);
}
@Override
public boolean getLatchMode() {
return outgoingMessageQueue.getLatchMode();
}
@Override
public void shutdown(long timeout, TimeUnit unit) {
signalOnShutdown(timeout, unit);
outgoingMessageQueue.shutdown();
listeners.shutdown();
}
@Override
public void shutdown() {
shutdown(DEFAULT_SHUTDOWN_TIMEOUT, DEFAULT_SHUTDOWN_TIMEOUT_UNITS);
}
public PublisherIdentifier getIdentifier() {
return new PublisherIdentifier(nodeIdentifier, getTopicDeclaration().getIdentifier());
}
public PublisherDeclaration toDeclaration() {
return PublisherDeclaration.newFromNodeIdentifier(nodeIdentifier, getTopicDeclaration());
}
@Override
public boolean hasSubscribers() {
return outgoingMessageQueue.getNumberOfChannels() > 0;
}
@Override
public int getNumberOfSubscribers() {
return outgoingMessageQueue.getNumberOfChannels();
}
@Override
public T newMessage() {
return messageFactory.newFromType(getTopicDeclaration().getMessageType());
}
@Override
public void publish(T message) {
if (log.isDebugEnabled()) {
log.debug(String.format("Publishing message %s on topic %s.", message, getTopicName()));
}
outgoingMessageQueue.add(message);
}
/**
* Complete connection handshake on buffer. This generates the connection
* header for this publisher to send and also updates the connection state of
* this publisher.
*
* @return encoded connection header from subscriber
*/
public ChannelBuffer finishHandshake(ConnectionHeader incomingHeader) {
ConnectionHeader topicDefinitionHeader = getTopicDeclarationHeader();
if (log.isDebugEnabled()) {
log.debug("Subscriber handshake header: " + incomingHeader);
log.debug("Publisher handshake header: " + topicDefinitionHeader);
}
// TODO(damonkohler): Return errors to the subscriber over the wire.
String incomingType = incomingHeader.getField(ConnectionHeaderFields.TYPE);
String expectedType = topicDefinitionHeader.getField(ConnectionHeaderFields.TYPE);
boolean messageTypeMatches =
incomingType.equals(expectedType) || incomingType.equals(Subscriber.TOPIC_MESSAGE_TYPE_WILDCARD);
Preconditions.checkState(messageTypeMatches, "Unexpected message type " + incomingType + " != " + expectedType);
String incomingChecksum = incomingHeader.getField(ConnectionHeaderFields.MD5_CHECKSUM);
String expectedChecksum = topicDefinitionHeader.getField(ConnectionHeaderFields.MD5_CHECKSUM);
boolean checksumMatches =
incomingChecksum.equals(expectedChecksum) || incomingChecksum.equals(Subscriber.TOPIC_MESSAGE_TYPE_WILDCARD);
Preconditions.checkState(checksumMatches, "Unexpected message MD5 " + incomingChecksum + " != " + expectedChecksum);
ConnectionHeader outgoingConnectionHeader = toDeclaration().toConnectionHeader();
// TODO(damonkohler): Force latch mode to be consistent throughout the
// life
// of the publisher.
outgoingConnectionHeader.addField(ConnectionHeaderFields.LATCHING, getLatchMode() ? "1" : "0");
return outgoingConnectionHeader.encode();
}
/**
* Add a {@link Subscriber} connection to this {@link Publisher}.
*
* @param subscriberIdentifer
* the {@link SubscriberIdentifier} of the new subscriber
* @param channel
* the communication {@link Channel} to the {@link Subscriber}
*/
public void addSubscriber(SubscriberIdentifier subscriberIdentifer, Channel channel) {
if (log.isDebugEnabled()) {
log.debug(String.format("Adding subscriber %s channel %s to publisher %s.", subscriberIdentifer, channel, this));
}
outgoingMessageQueue.addChannel(channel);
signalOnNewSubscriber(subscriberIdentifer);
}
@Override
public void addListener(PublisherListener<T> listener) {
listeners.add(listener);
}
/**
* Signal all {@link PublisherListener}s that the {@link Publisher} has
* successfully registered with the master.
* <p>
* Each listener is called in a separate thread.
*/
@Override
public void signalOnMasterRegistrationSuccess() {
final Publisher<T> publisher = this;
listeners.signal(new SignalRunnable<PublisherListener<T>>() {
@Override
public void run(PublisherListener<T> listener) {
listener.onMasterRegistrationSuccess(publisher);
}
});
}
/**
* Signal all {@link PublisherListener}s that the {@link Publisher} has failed
* to register with the master.
* <p>
* Each listener is called in a separate thread.
*/
@Override
public void signalOnMasterRegistrationFailure() {
final Publisher<T> publisher = this;
listeners.signal(new SignalRunnable<PublisherListener<T>>() {
@Override
public void run(PublisherListener<T> listener) {
listener.onMasterRegistrationFailure(publisher);
}
});
}
/**
* Signal all {@link PublisherListener}s that the {@link Publisher} has
* successfully unregistered with the master.
* <p>
* Each listener is called in a separate thread.
*/
@Override
public void signalOnMasterUnregistrationSuccess() {
final Publisher<T> publisher = this;
listeners.signal(new SignalRunnable<PublisherListener<T>>() {
@Override
public void run(PublisherListener<T> listener) {
listener.onMasterUnregistrationSuccess(publisher);
}
});
}
/**
* Signal all {@link PublisherListener}s that the {@link Publisher} has failed
* to unregister with the master.
* <p>
* Each listener is called in a separate thread.
*/
@Override
public void signalOnMasterUnregistrationFailure() {
final Publisher<T> publisher = this;
listeners.signal(new SignalRunnable<PublisherListener<T>>() {
@Override
public void run(PublisherListener<T> listener) {
listener.onMasterUnregistrationFailure(publisher);
}
});
}
/**
* Signal all {@link PublisherListener}s that the {@link Publisher} has a new
* {@link Subscriber}.
* <p>
* Each listener is called in a separate thread.
*
* @param subscriberIdentifier
* the {@link SubscriberIdentifier} of the new {@link Subscriber}
*/
private void signalOnNewSubscriber(final SubscriberIdentifier subscriberIdentifier) {
final Publisher<T> publisher = this;
listeners.signal(new SignalRunnable<PublisherListener<T>>() {
@Override
public void run(PublisherListener<T> listener) {
listener.onNewSubscriber(publisher, subscriberIdentifier);
}
});
}
/**
* Signal all {@link PublisherListener}s that the {@link Publisher} is being
* shut down. Listeners should exit quickly since they may block shut down.
* <p>
* Each listener is called in a separate thread.
*
* @param timeout
* @param unit
*/
private void signalOnShutdown(long timeout, TimeUnit unit) {
final Publisher<T> publisher = this;
try {
listeners.signal(new SignalRunnable<PublisherListener<T>>() {
@Override
public void run(PublisherListener<T> listener) {
listener.onShutdown(publisher);
}
}, timeout, unit);
} catch (InterruptedException e) {
// Ignored since we do not guarantee that all listeners will finish
// before shutdown begins.
}
}
@Override
public String toString() {
return "Publisher<" + toDeclaration() + ">";
}
}