package com.rayo.server.servlet;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.UUID;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import org.apache.xerces.dom.CoreDocumentImpl;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.dom.DOMElement;
import org.dom4j.io.DOMWriter;
import com.rayo.core.HangupCommand;
import com.rayo.core.OfferEvent;
import com.rayo.server.JIDRegistry;
import com.rayo.server.MixerManager;
import com.rayo.server.Server;
import com.rayo.server.Transport;
import com.rayo.server.TransportCallback;
import com.rayo.server.admin.RayoAdminService;
import com.rayo.server.exception.RayoProtocolException;
import com.rayo.server.listener.XmppMessageListenerGroup;
import com.rayo.server.lookup.RayoJIDLookupService;
import com.voxeo.logging.Loggerf;
import com.voxeo.servlet.xmpp.IQRequest;
import com.voxeo.servlet.xmpp.IQResponse;
import com.voxeo.servlet.xmpp.InstantMessage;
import com.voxeo.servlet.xmpp.JID;
import com.voxeo.servlet.xmpp.PresenceMessage;
@SuppressWarnings("serial")
public class RayoServlet extends AbstractRayoServlet implements Transport {
protected static final Loggerf log = Loggerf.getLogger(RayoServlet.class);
/**
* By default a Rayo Server will retry 5 times in case of any failure
* while brodcasting its status
*/
public static final int BROADCAST_RETRIES = 5; // 5 retries to broadcast node state
/**
* In case of a brodcast error by default there will be a 30 seconds delay
* between broadcasting retries
*/
public static final int BROADCAST_RETRY_DELAY = 30000;
private Server server;
private JIDRegistry jidRegistry;
private MixerManager mixerManager;
private RayoJIDLookupService<OfferEvent> rayoLookupService;
private XmppMessageListenerGroup xmppMessageListenersGroup;
private int broadcastRetries = BROADCAST_RETRIES;
private int broadcastRetryDelay = BROADCAST_RETRY_DELAY;
// Setup
// ================================================================================
@Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
RayoAdminService adminService = (RayoAdminService)getAdminService();
String gatewayDomain = adminService.getGatewayDomain();
if (gatewayDomain != null) {
// It will be the gateway who takes care of disposing mixers
mixerManager.setGatewayHandlingMixers(true);
}
broadcastPresence("chat");
}
public void start() {
super.start();
server.addTransport(this);
}
// Events: Server -> Client
// ================================================================================
@Override
public boolean callEvent(String callId, String componentId, Element body) throws Exception {
JID jid = null;
JID from = null;
RayoAdminService adminService = (RayoAdminService) getAdminService();
String gatewayDomain = adminService.getGatewayDomain();
if (gatewayDomain == null) {
// Single server. This code needs a bit of refactoring specially as
// the gateway servlet shares some of this stuff.
if (body.getName().equals("offer")) {
URI to = new URI(body.attributeValue("to"));
JID callTo = getCallDestination(to);
if (callTo == null) {
log.error("Could not deliver event. Call id [%s] is not being managed by Rayo. Skipping event");
return false;
}
jidRegistry.put(callId, callTo);
}
// Call Cleanup
else if(body.getName().equals("end")) {
jidRegistry.remove(callId);
}
String callDomain = getLocalDomain();
jid = jidRegistry.getJID(callId);
from = getXmppFactory().createJID(callId + "@" + callDomain);
}
else {
// Clustered setup. Everything is forwarded to the gateway
from = getXmppFactory().createJID(callId + "@" + getLocalDomain());
jid = getXmppFactory().createJID(gatewayDomain);
}
from.setResource(componentId);
// TODO: ouch
try {
org.w3c.dom.Element documentElement = toDomElement(body);
PresenceMessage presence = getXmppFactory().createPresence(from, jid, null, documentElement);
if (presence != null) {
presence.send();
xmppMessageListenersGroup.onPresenceSent(presence);
} else {
log.warn("Could not send Presence : %s. Is there a client listening for that call?", presence);
}
} catch (ServletException e) {
if (e.getMessage().startsWith("can't find corresponding client session") &&
body.getName().equals("offer")) {
log.warn("Could not handle offer. No Rayo clients listening on XMPP.");
jidRegistry.remove(callId);
return false;
}
}
return true;
}
@Override
public boolean mixerEvent(String mixerId, Collection<String> participants, Element body) {
JID from = getXmppFactory().createJID(mixerId + "@" + getLocalDomain());
RayoAdminService adminService = (RayoAdminService) getAdminService();
String gatewayDomain = adminService.getGatewayDomain();
List<JID> destinations = new ArrayList<JID>();
// Single server. Deliver individual messages to all apps listening.
// Note that mutiple callIds might belong to the same app so we need
// to multiplex the mixer event destinations
if (gatewayDomain == null) {
for (String callId : participants) {
JID jid = jidRegistry.getJID(callId);
if (jid != null) {
if (!destinations.contains(jid)) {
destinations.add(jid);
}
}
}
}
// Clustered setup. Everything is forwarded to the gateway. The Gateway
// will take care of multiplexing
else {
destinations.add(getXmppFactory().createJID(gatewayDomain));
}
for (JID jid : destinations) {
try {
// TODO: ouch
org.w3c.dom.Element documentElement = toDomElement(body);
PresenceMessage presence = getXmppFactory().createPresence(from, jid, null, documentElement);
presence.send();
xmppMessageListenersGroup.onPresenceSent(presence);
}
catch (ServletException se) {
log.error(se.getMessage(), se);
}
catch (Exception e) {
// In the event of an error, continue dispatching to all remaining JIDs
log.error("Failed to dispatch event [jid=%s, event=%s]", jid, body.asXML(), e);
}
}
return true;
}
// Commands: Client -> Server
// ================================================================================
@Override
protected void processIQRequest(final IQRequest request, DOMElement payload) {
xmppMessageListenersGroup.onIQReceived(request);
// Handle outbound 'dial' command
if (payload.getName().equals("dial")) {
server.handleCommand(null, null, payload, new TransportCallback() {
public void handle(Element result, Exception err) {
if (err != null) {
sendIqError(request, err);
} else {
String callId = result.attributeValue("id");
jidRegistry.put(callId, request.getFrom().getBareJID());
sendIqResult(request, toDomElement(result));
}
}
});
return;
}
//TODO: Refactor this. Right now it is necessary to get mixers output working.
else if (payload.getName().equals("join")) {
String mixerName = payload.attributeValue("mixer-name");
if (mixerName != null) {
jidRegistry.put(mixerName, request.getFrom().getBareJID());
}
}
// Extract Call ID
String callId = request.getTo().getNode();
// Extract Component ID
String componentId = null;
if(request.getTo().getResource() != null) {
componentId = request.getTo().getResource();
}
else {
componentId = UUID.randomUUID().toString();
}
server.handleCommand(callId, componentId, payload, new TransportCallback() {
public void handle(Element result, Exception err) {
if(err != null) {
sendIqError(request, err);
}
else if (result == null) {
sendIqResult(request, null);
}
else {
sendIqResult(request, toDomElement(result));
}
}
});
}
public JID getCallDestination(URI offerTo) throws RayoProtocolException{
String forwardDestination = rayoLookupService.lookup(offerTo);
if (forwardDestination == null) {
throw new RayoProtocolException(
com.rayo.server.exception.RayoProtocolException.Condition.ITEM_NOT_FOUND,
"No application mapped to target address [address=" + offerTo.toString() + "]");
}
getLog().debug("Received Offer. Offer will be delivered to [%s]", forwardDestination);
return getXmppFactory().createJID(forwardDestination);
}
// Routing and Presence
// ================================================================================
@Override
public void onPropertyChanged(String property, String newValue) {
if (property.equals(RayoAdminService.WEIGHT) ||
property.equals(RayoAdminService.PRIORITY) ||
property.equals(RayoAdminService.DEFAULT_PLATFORM_ID)) {
broadcastPresence("chat");
}
}
@Override
public void onQuiesceModeEntered() {
broadcastPresence("away");
}
@Override
public void onQuiesceModeExited() {
broadcastPresence("chat");
}
@Override
public void onShutdown() {
broadcastPresence("unavailable");
}
/**
* Broadcasts presence of this Rayo Node to the configured Rayo Gateway
*
* @param status Presence status to be broadcasted
*/
private void broadcastPresence(final String status) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
public void run() {
doPresenceBroadcast(status);
}
},0);
}
private void appendNodeInfoElement(CoreDocumentImpl document, org.w3c.dom.Element nodeInfoElement, String name, String value) {
org.w3c.dom.Element platform = document.createElement(name);
platform.setTextContent(value);
nodeInfoElement.appendChild(platform);
}
private void doPresenceBroadcast(final String status) {
RayoAdminService adminService = (RayoAdminService)getAdminService();
String gatewayDomain = adminService.getGatewayDomain();
if (adminService.getGatewayDomain() != null) {
int retries = 0;
do {
PresenceMessage presence = null;
try {
if (status.equalsIgnoreCase("unavailable")) {
presence = getXmppFactory().createPresence(getLocalDomain(), gatewayDomain, "unavailable", (org.w3c.dom.Element)null);
} else {
CoreDocumentImpl document = new CoreDocumentImpl(false);
org.w3c.dom.Element showElement = document.createElement("show");
showElement.setTextContent(status.toLowerCase());
org.w3c.dom.Element nodeInfoElement = document.createElementNS("urn:xmpp:rayo:cluster:1", "node-info");
appendNodeInfoElement(document, nodeInfoElement, "platform", adminService.getPlatform());
appendNodeInfoElement(document, nodeInfoElement, "weight", adminService.getWeight());
appendNodeInfoElement(document, nodeInfoElement, "priority", adminService.getPriority());
presence = getXmppFactory().createPresence(getLocalDomain(), gatewayDomain, null, showElement, nodeInfoElement);
}
presence.send();
break;
} catch (Exception e) {
log.error("Could not broadcast presence to gateway [%s]", gatewayDomain, e);
try {
Thread.sleep(broadcastRetryDelay);
} catch (InterruptedException e1) {}
retries++;
}
} while(retries < broadcastRetries);
}
}
@Override
protected void doMessage(InstantMessage message) throws ServletException, IOException {
server.getRayoStatistics().messageStanzaReceived();
}
@Override
protected void doPresence(PresenceMessage presence) throws ServletException, IOException {
server.getRayoStatistics().presenceStanzaReceived();
JID toJid = presence.getTo();
JID fromJid = presence.getFrom();
RayoAdminService adminService = (RayoAdminService)getAdminService();
String gatewayDomain = adminService.getGatewayDomain();
if (fromJid.getNode() == null) {
if (gatewayDomain != null && fromJid.getDomain().equals(gatewayDomain)) {
if (presence.getType().equals("error")) {
server.getRayoStatistics().presenceErrorReceived();
String callId = toJid.getNode();
if (callId != null) {
HangupCommand command = new HangupCommand();
command.setCallId(callId);
// No calback. Nothing we can really do if hangup fails.
server.handleCommand(callId, null, command, null);
}
} else {
log.warn("Ignoring presence message from Gateay");
}
} else {
log.warn("Ignoring presence message from unknown domain");
}
} else {
log.warn("Ignoring unknown presence message");
}
}
// Util
// ================================================================================
@Override
protected void sendIqError(IQRequest request, IQResponse response) throws IOException {
server.getRayoStatistics().iqError();
super.sendIqError(request, response);
xmppMessageListenersGroup.onErrorSent(response);
}
@Override
protected IQResponse sendIqResult(IQRequest request, org.w3c.dom.Element result) {
try {
server.getRayoStatistics().iqResult();
IQResponse response = super.sendIqResult(request, result);
xmppMessageListenersGroup.onIQSent(response);
return response;
}
catch (IOException e) {
throw new IllegalStateException("Cannot dispatch result", e);
}
}
private org.w3c.dom.Element toDomElement(Element resultElement) {
try {
return new DOMWriter().write(resultElement.getDocument()).getDocumentElement();
}
catch (DocumentException e) {
throw new IllegalStateException(e);
}
}
// Properties
// ================================================================================
public void setJidRegistry(JIDRegistry jidRegistry) {
this.jidRegistry = jidRegistry;
}
public void setXmppMessageListenersGroup(XmppMessageListenerGroup xmppMessageListenersGroup) {
this.xmppMessageListenersGroup = xmppMessageListenersGroup;
}
@Override
protected Loggerf getLog() {
return log;
}
public void setBroadcastRetries(int broadcastRetries) {
this.broadcastRetries = broadcastRetries;
}
public void setBroadcastRetryDelay(int broadcastRetryDelay) {
this.broadcastRetryDelay = broadcastRetryDelay;
}
public void setRayoLookupService(RayoJIDLookupService<OfferEvent> rayoLookupService) {
this.rayoLookupService = rayoLookupService;
}
public MixerManager getMixerManager() {
return mixerManager;
}
public void setMixerManager(MixerManager mixerManager) {
this.mixerManager = mixerManager;
}
public void setServer(Server server) {
this.server = server;
}
}