/**
*
*/
package vnet.sms.gateway.nettysupport.publish.outgoing;
import static org.apache.commons.lang.Validate.notNull;
import java.io.Serializable;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.TimeUnit;
import javax.annotation.PreDestroy;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelFuture;
import org.jboss.netty.channel.ChannelFutureListener;
import org.jboss.netty.channel.group.ChannelGroup;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jmx.export.annotation.ManagedOperation;
import org.springframework.jmx.export.annotation.ManagedOperationParameter;
import org.springframework.jmx.export.annotation.ManagedOperationParameters;
import org.springframework.jmx.export.annotation.ManagedResource;
import vnet.sms.common.messages.GsmPdu;
import vnet.sms.common.messages.Msisdn;
import vnet.sms.common.messages.Sms;
import vnet.sms.common.wme.acknowledge.MessageAcknowledgementContainer;
import vnet.sms.common.wme.acknowledge.SendSmsAckContainer;
import vnet.sms.common.wme.acknowledge.SendSmsNackContainer;
import vnet.sms.common.wme.send.SendSmsContainer;
import vnet.sms.gateway.nettysupport.Jmx;
import com.yammer.metrics.Metrics;
import com.yammer.metrics.core.Meter;
import com.yammer.metrics.core.Metric;
import com.yammer.metrics.core.MetricName;
import com.yammer.metrics.core.Timer;
import com.yammer.metrics.core.TimerContext;
/**
* @author obergner
*
*/
@ManagedResource(objectName = DefaultOutgoingMessagesSender.OBJECT_NAME)
public class DefaultOutgoingMessagesSender<ID extends Serializable> implements
OutgoingMessagesSender<ID> {
// ------------------------------------------------------------------------
// Static
// ------------------------------------------------------------------------
private static final String TYPE = "OutgoingMessagesSender";
private static final String NAME = "DEFAULT";
static final String OBJECT_NAME = Jmx.GROUP
+ ":type="
+ TYPE
+ ",name="
+ NAME;
// ------------------------------------------------------------------------
// Instance
// ------------------------------------------------------------------------
private final Logger log = LoggerFactory
.getLogger(getClass());
private final Set<OutgoingMessagesSender.Listener<ID>> listeners = new CopyOnWriteArraySet<OutgoingMessagesSender.Listener<ID>>();
private final Meter numberOfSentSms = Metrics
.newMeter(
new MetricName(
Jmx.GROUP,
TYPE,
"acknowledgement-send-count"),
"acknowledgement-sent",
TimeUnit.SECONDS);
private final Timer smsSendDuration = Metrics
.newTimer(
new MetricName(
Jmx.GROUP,
TYPE,
"acknowledgement-send-duration"),
TimeUnit.MILLISECONDS,
TimeUnit.SECONDS);
private final Meter numberOfSentAcknowledgements = Metrics
.newMeter(
new MetricName(
Jmx.GROUP,
TYPE,
"acknowledgement-send-count"),
"acknowledgement-sent",
TimeUnit.SECONDS);
private final Timer acknowledgementSendDuration = Metrics
.newTimer(
new MetricName(
Jmx.GROUP,
TYPE,
"acknowledgement-send-duration"),
TimeUnit.MILLISECONDS,
TimeUnit.SECONDS);
private final ChannelGroup allConnectedChannels;
// ------------------------------------------------------------------------
// Constructors
// ------------------------------------------------------------------------
/**
* @param allConnectedChannels
*/
public DefaultOutgoingMessagesSender(final ChannelGroup allConnectedChannels) {
notNull(allConnectedChannels,
"Argument 'allConnectedChannels' must not be null");
this.allConnectedChannels = allConnectedChannels;
}
// ------------------------------------------------------------------------
// Managing listeners
// ------------------------------------------------------------------------
@Override
public boolean addListener(
final OutgoingMessagesSender.Listener<ID> listener) {
this.log.info("Added listener {}", listener);
return this.listeners.add(listener);
}
@Override
public boolean removeListener(
final OutgoingMessagesSender.Listener<ID> listener) {
this.log.info("Removed listener {}", listener);
return this.listeners.remove(listener);
}
@Override
public void clearListeners() {
this.log.info("Clearing [{}] listeners", this.listeners.size());
this.listeners.clear();
}
// ------------------------------------------------------------------------
// Sending messages
// ------------------------------------------------------------------------
/**
* @see vnet.sms.gateway.nettysupport.publish.outgoing.OutgoingMessagesSender#sendSms(vnet.sms.common.wme.send.SendSmsContainer)
*/
@Override
public ChannelFuture sendSms(final SendSmsContainer sms) throws Exception {
notNull(sms, "Argument 'acknowledgement' must not be null");
try {
this.log.debug("Sending {} ...", sms);
final Channel channel = selectRandomChannel();
this.log.debug("Will send {} via {}", sms, channel);
final TimerContext smsSendTimer = this.smsSendDuration.time();
final ChannelFuture smsHasBeenSent = channel.write(sms);
smsHasBeenSent.addListener(new SendSmsFuture(sms, smsSendTimer));
return smsHasBeenSent;
} catch (final Exception e) {
fireSmsSendFailed(sms, e);
throw e;
}
}
private Channel selectRandomChannel() throws IllegalStateException {
try {
return this.allConnectedChannels.iterator().next();
} catch (final NoSuchElementException e) {
throw new IllegalStateException(
"No channel is currently connected - cannot determine channel via which to send message");
}
}
private void fireSmsSendFailed(final SendSmsContainer failedSms,
final Throwable error) {
for (final Listener<ID> listener : this.listeners) {
listener.sendSmsFailed(failedSms, error);
}
}
private final class SendSmsFuture implements ChannelFutureListener {
private final SendSmsContainer sms;
private final TimerContext smsSendTimer;
private SendSmsFuture(final SendSmsContainer sms,
final TimerContext smsSendTimer) {
this.sms = sms;
this.smsSendTimer = smsSendTimer;
}
@Override
public void operationComplete(final ChannelFuture future)
throws Exception {
this.smsSendTimer.stop();
if (!future.isSuccess()) {
DefaultOutgoingMessagesSender.this.log.error("Sending "
+ this.sms + " failed: "
+ future.getCause().getMessage(), future.getCause());
fireSmsSendFailed(this.sms, future.getCause());
} else {
DefaultOutgoingMessagesSender.this.log.debug(
"Successfully sent {} via {}", this.sms,
future.getChannel());
DefaultOutgoingMessagesSender.this.numberOfSentSms.mark();
}
}
}
@Override
public ChannelFuture ackReceivedSms(final SendSmsAckContainer<ID> ack)
throws Exception {
notNull(ack, "Argument 'ack' must not be null");
try {
this.log.debug("Sending {} ...", ack);
final Channel channel = replyChannelFor(ack);
this.log.debug("Will send {} via {}", ack, channel);
final TimerContext ackSendTimer = this.acknowledgementSendDuration
.time();
final ChannelFuture ackHasBeenSent = channel.write(ack);
ackHasBeenSent.addListener(new AcknowledgeReceivedSmsFuture(ack,
ackSendTimer));
return ackHasBeenSent;
} catch (final Exception e) {
fireAcknowledgeReceivedSmsFailed(ack, e);
throw e;
}
}
private Channel replyChannelFor(
final MessageAcknowledgementContainer<ID, ? extends GsmPdu> acknowledgement) {
final Channel replyChannel = this.allConnectedChannels
.find(acknowledgement.getReceivingChannelId());
if (replyChannel == null) {
throw new IllegalStateException(
"Cannot send acknowledgement "
+ acknowledgement
+ " for previously received message "
+ acknowledgement.getAcknowledgedMessage()
+ " via channel having ID = ["
+ acknowledgement.getReceivingChannelId()
+ "] (the channel on which the acknowledged message has been received) since this channel is not connected anymore");
}
return replyChannel;
}
private final class AcknowledgeReceivedSmsFuture implements
ChannelFutureListener {
private final MessageAcknowledgementContainer<ID, Sms> acknowledgement;
private final TimerContext acknowledgementSendTimer;
private AcknowledgeReceivedSmsFuture(
final MessageAcknowledgementContainer<ID, Sms> acknowledgement,
final TimerContext acknowledgementSendTimer) {
this.acknowledgement = acknowledgement;
this.acknowledgementSendTimer = acknowledgementSendTimer;
}
@Override
public void operationComplete(final ChannelFuture future)
throws Exception {
this.acknowledgementSendTimer.stop();
if (!future.isSuccess()) {
DefaultOutgoingMessagesSender.this.log.error("Sending "
+ this.acknowledgement + " failed: "
+ future.getCause().getMessage(), future.getCause());
fireAcknowledgeReceivedSmsFailed(this.acknowledgement,
future.getCause());
} else {
DefaultOutgoingMessagesSender.this.log.debug(
"Successfully sent {} via {}", this.acknowledgement,
future.getChannel());
DefaultOutgoingMessagesSender.this.numberOfSentAcknowledgements
.mark();
}
}
}
private void fireAcknowledgeReceivedSmsFailed(
final MessageAcknowledgementContainer<ID, Sms> failedAcknowledgement,
final Throwable error) {
for (final Listener<ID> listener : this.listeners) {
listener.acknowldgeReceivedSmsFailed(failedAcknowledgement, error);
}
}
@Override
public ChannelFuture nackReceivedSms(final SendSmsNackContainer<ID> nack)
throws Exception {
notNull(nack, "Argument 'nack' must not be null");
try {
this.log.debug("Sending {} ...", nack);
final Channel channel = replyChannelFor(nack);
this.log.debug("Will send {} via {}", nack, channel);
final TimerContext ackSendTimer = this.acknowledgementSendDuration
.time();
final ChannelFuture ackHasBeenSent = channel.write(nack);
ackHasBeenSent.addListener(new AcknowledgeReceivedSmsFuture(nack,
ackSendTimer));
return ackHasBeenSent;
} catch (final Exception e) {
fireAcknowledgeReceivedSmsFailed(nack, e);
throw e;
}
}
// ------------------------------------------------------------------------
// JMX API
// ------------------------------------------------------------------------
// FIXME: This method does currently fill neither originator nor destination
// MSISDN
@ManagedOperation(description = "Send an SMS")
@ManagedOperationParameters({
@ManagedOperationParameter(name = "originator", description = "Sender's MSISDN"),
@ManagedOperationParameter(name = "destination", description = "Receiver's MSISDN"),
@ManagedOperationParameter(name = "text", description = "The text to send") })
public void sendSms(final String originator, final String destination,
final String text) throws Exception {
this.log.info("Received request to send SMS [\"{}\"] via JMX", text);
final Sms sms = new Sms(new Msisdn(originator),
new Msisdn(destination), text);
final SendSmsContainer sendSmsContainer = new SendSmsContainer(sms);
final ChannelFuture smsSent = sendSms(sendSmsContainer);
this.log.info(
"{} has been sent asynchronously - will await successful sent",
sms);
smsSent.awaitUninterruptibly();
}
// ------------------------------------------------------------------------
// Clean up resources
// ------------------------------------------------------------------------
@Override
@PreDestroy
public void close() {
this.log.info("Closing OutgoingMessagesSender - will remove all statistics from MBean server ...");
Metrics.defaultRegistry().removeMetric(
metricNameOf(this.numberOfSentSms));
Metrics.defaultRegistry().removeMetric(
metricNameOf(this.smsSendDuration));
Metrics.defaultRegistry().removeMetric(
metricNameOf(this.numberOfSentAcknowledgements));
Metrics.defaultRegistry().removeMetric(
metricNameOf(this.acknowledgementSendDuration));
this.log.info("OutgoingMessagesSender closed - all statistics have been removed from MBean server");
}
private MetricName metricNameOf(final Metric metric) {
for (final Map.Entry<MetricName, Metric> namePlusMetric : Metrics
.defaultRegistry().allMetrics().entrySet()) {
if (namePlusMetric.getValue().equals(metric)) {
return namePlusMetric.getKey();
}
}
throw new IllegalArgumentException("Metric [" + metric
+ "] has not been registered in MetricsRegistry ["
+ Metrics.defaultRegistry() + "]");
}
}