package com.voxeo.tropo.app;
import java.io.IOException;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import javax.script.ScriptException;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.sip.SipServletRequest;
import javax.servlet.sip.SipURI;
import javax.servlet.sip.TelURL;
import javax.servlet.sip.URI;
import javax.xml.ws.soap.SOAPFaultException;
import org.apache.log4j.Logger;
import org.apache.thrift.TException;
import org.apache.thrift.server.TServer;
import org.apache.thrift.server.TThreadPoolServer;
import org.apache.thrift.transport.TServerSocket;
import org.apache.thrift.transport.TServerTransport;
import com.micromethod.common.util.StringUtils;
import com.voxeo.tropo.core.CallImpl;
import com.voxeo.tropo.thrift.AlertStruct;
import com.voxeo.tropo.thrift.AuthenticationException;
import com.voxeo.tropo.thrift.BindException;
import com.voxeo.tropo.thrift.BindStruct;
import com.voxeo.tropo.thrift.HangupStruct;
import com.voxeo.tropo.thrift.PromptStruct;
import com.voxeo.tropo.thrift.SystemException;
import com.voxeo.tropo.thrift.TransferStruct;
import com.voxeo.tropo.thrift.TropoException;
import com.voxeo.tropo.thrift.TropoService;
import com.voxeo.tropo.transport.reversetcp.TSocketFactory;
import com.voxeo.tropo.util.Utils;
import com.voxeo.webcore.dns.URLByNumberGet;
import com.voxeo.webcore.dns.URLByTokenGet;
import com.voxeo.webcore.dns.VoxeoDNSException;
public class ThriftAppMgr extends AbstractRemoteApplicationManager implements Runnable, TropoService.Iface {
private static final Logger LOG = Logger.getLogger(ThriftAppMgr.class);
static final String DEAULT_APPLICATION_ID = "*";
protected ConcurrentMap<Integer, ConcurrentMap<String, RemoteApplication>> _apps;
protected Map<String, RemoteApplication> _index;
//protected Map<String, ApplicationInstance> _insts;
protected TServer _server;
protected TServerTransport _transport;
protected List<SipURI> _contacts;
protected int _serverPort = 9090;
protected ApplicationCollector _collector;
protected String _authenticationServer = "http://evolution-internal.voxeo.com/services/AccountManagement?wsdl";
@Override
protected Application findApplication(URI uri) throws InvalidApplicationException, RedirectException {
String name = null;
if (uri instanceof SipURI) {
name = ((SipURI) uri).getUser();
}
else if (uri instanceof TelURL) {
name = ((TelURL) uri).getPhoneNumber();
}
else {
throw new InvalidApplicationException("Only SIP URI or Tel URL is supported: " + uri);
}
try {
URLByNumberGet info = new URLByNumberGet();
info.execute(name);
RemoteApplication app = null;
try {
app = getApplication(info.getAccountID(), info.getApplicationID());
}
catch(Exception e) {
throw new InvalidApplicationException(e);
}
if (app != null) {
if (!app.isProxy()) {
LOG.info(this + " found remote " + app + " for " + uri);
return app;
}
else {
throw new RedirectException(app.getContacts());
}
}
throw new InvalidApplicationException("No application has been bound for " + uri);
}
catch(VoxeoDNSException e) {
LOG.error(e.toString(), e);
throw new InvalidApplicationException("Unable to retrieve DNS records for " + uri);
}
}
@Override
public String toString() {
return "Tropo(Remote Edition)";
}
@Override
protected Application findApplication(String token, Properties params) throws InvalidApplicationException, RedirectException {
URLByTokenGet info = new URLByTokenGet();
info.execute(token);
RemoteApplication app = null;
try {
app = getApplication(info.getAccountID(), info.getApplicationID());
}
catch(Exception e) {
throw new InvalidApplicationException(e);
}
if (app != null) {
if (!app.isProxy()) {
LOG.info(this + " found remote " + app + " for " + token);
return app;
}
else {
throw new RedirectException(app.getContacts());
}
}
throw new InvalidApplicationException("No application has been bound for " + token);
}
@SuppressWarnings("unchecked")
@Override
public void init(final ServletContext context, final Map<String, String> paras) {
String wsdl = paras.remove("authenticationServer");
if (wsdl != null && wsdl.length() > 0) {
_authenticationServer = wsdl.trim();
}
String sport = paras.remove("thriftServicePort");
if (sport != null && sport.length() > 0) {
_serverPort = Integer.parseInt(sport);
}
super.init(context, paras);
_contacts = (List<SipURI>) context.getAttribute("javax.servlet.sip.outboundInterfaces");
_apps = new ConcurrentHashMap<Integer, ConcurrentMap<String, RemoteApplication>>();
_index = new ConcurrentHashMap<String, RemoteApplication>();
try {
_transport = new TServerSocket(_serverPort);
_server = new TThreadPoolServer(new TropoService.Processor(this), _transport);
new Thread(this, "Thrift").start();
_collector = new ApplicationCollector();
new Thread(_collector, "ApplicationCollector").start();
// init the reverse TCP connection factory
TSocketFactory.getInstance(_serverPort + 1);
}
catch(Exception e) {
LOG.error(e.toString(), e);
throw new RuntimeException(e);
}
}
@Override
public void dispose() {
_transport.close();
_server.stop();
_collector.stop();
_apps.clear();
TSocketFactory.shutdown();
super.dispose();
}
public static synchronized void initialization(final ServletContext ctx) throws InstantiationException, IllegalAccessException {
AbstractApplicationManager.initialization(ctx);
}
public void run() {
LOG.info("Starting Thrift service on port " + _serverPort);
_server.serve();
LOG.info("Stopping Thrift service on port " + _serverPort);
}
public void answer(String key, String id, int timeout) throws AuthenticationException, TropoException, SystemException, TException {
RemoteApplication app = _index.get(key);
if (app != null && app instanceof ThriftApplication) {
((ThriftApplication)app).answer(id, timeout);
}
else {
throw new AuthenticationException("Invalid key: " + key);
}
}
public void startCallRecording(String key, String id, String recordUri, String recordFormat)
throws AuthenticationException, TropoException, SystemException, TException {
RemoteApplication app = _index.get(key);
if (app != null && app instanceof ThriftApplication) {
((ThriftApplication) app).startCallRecording(id, recordUri, recordFormat, null, null);
}
else {
throw new AuthenticationException("Invalid key: " + key);
}
}
public void stopCallRecording(String key, String id) throws AuthenticationException, TropoException, SystemException,
TException {
RemoteApplication app = _index.get(key);
if (app != null && app instanceof ThriftApplication) {
((ThriftApplication) app).stopCallRecording(id);
}
else {
throw new AuthenticationException("Invalid key: " + key);
}
}
protected void authenticate(BindStruct bind) throws AuthenticationException, SystemException {
if (bind.getAccountId() <= 0 || StringUtils.isEmpty(bind.getApplicationId())) {
throw new SystemException("Invalid account or application.");
}
try {
String token = Utils.authenticate(_authenticationServer, bind.getUser(), bind.getPassword());
}
catch (SOAPFaultException e) {
if (e.getMessage().contains("Invalid login")) {
if (LOG.isDebugEnabled()) {
LOG.debug("Invalid username[" + bind.getUser() + "] or password[" + bind.getPassword() + "]");
}
throw new AuthenticationException("Invalid username[" + bind.getUser() + "] or password[" + bind.getPassword() + "]");
}
else {
LOG.error(e.toString(), e);
throw new SystemException(e.getMessage());
}
}
catch (Exception e) {
LOG.error(e.toString(), e);
throw new SystemException(e.getMessage());
}
}
public String bind(BindStruct bind) throws AuthenticationException, BindException, TException, SystemException {
Utils.setLogContext(String.valueOf(bind.getAccountId()), "-1", "-1", "-1");
if (LOG.isDebugEnabled()) {
LOG.debug("bind-->" + bind.toString());
}
authenticate(bind);
int accountId = bind.getAccountId();
String applicationId = bind.getApplicationId();
RemoteApplication oldapp = getApplication(accountId, applicationId);
RemoteApplication newapp = new ThriftApplication(this, new ThriftURL(bind), accountId, applicationId, _contacts);
LOG.info(newapp.toString() + " has been created by " + bind.toString());
putApplication(newapp);
if (oldapp != null) {
removeApplication(oldapp);
oldapp.dispose();
}
return newapp.getApplicationKey();
}
protected RemoteApplication getApplication(int accountId, String applicationId) throws AuthenticationException, SystemException, TException {
ConcurrentMap<String, RemoteApplication> map = _apps.get(accountId);
if (map == null) {
map = new ConcurrentHashMap<String, RemoteApplication>();
_apps.putIfAbsent(accountId, map);
}
RemoteApplication app = map.get(applicationId);
if (app == null) {
app = map.get(DEAULT_APPLICATION_ID);
if (app != null && app instanceof ThriftApplication) {
app = new ThriftApplication((ThriftApplication)app, applicationId);
}
}
return app;
}
protected void putApplication(RemoteApplication app) {
ConcurrentMap<String, RemoteApplication> map = _apps.get(app.getAccountID());
if (map == null) {
map = new ConcurrentHashMap<String, RemoteApplication>();
_apps.putIfAbsent(app.getAccountID(), map);
}
map.put(app.getApplicationID(), app);
_index.put(app.getApplicationKey(), app);
clearCached(app);
}
protected void removeApplication(RemoteApplication app) {
ConcurrentMap<String, RemoteApplication> map = _apps.get(app.getAccountID());
if (map != null) {
map.remove(app.getApplicationID(), app);
}
_index.remove(app.getApplicationKey());
clearCached(app);
}
public Map<String, String> prompt(String key, String id, PromptStruct prompt) throws AuthenticationException, TropoException, SystemException, TException {
RemoteApplication app = _index.get(key);
if (app != null && app instanceof ThriftApplication) {
return ((ThriftApplication)app).prompt(id, prompt);
}
else {
throw new AuthenticationException("Invalid key: " + key);
}
}
public void redirect(String key, String id, String number) throws TropoException, AuthenticationException, SystemException, TException {
RemoteApplication app = _index.get(key);
if (app != null && app instanceof ThriftApplication) {
((ThriftApplication)app).redirect(id, number);
}
else {
throw new AuthenticationException("Invalid key: " + key);
}
}
public void reject(String key, String id) throws AuthenticationException, TropoException, SystemException, TException {
RemoteApplication app = _index.get(key);
if (app != null && app instanceof ThriftApplication) {
((ThriftApplication)app).reject(id);
}
else {
throw new AuthenticationException("Invalid key: " + key);
}
}
public void unbind(String token) throws TException {
RemoteApplication app = _index.get(token);
if (app != null) {
Utils.setLogContext(app, null);
LOG.info(app + " is disconnecting.");
removeApplication(app);
app.dispose();
}
}
public void heartbeat(String token) throws AuthenticationException, SystemException, TException {
RemoteApplication app = _index.get(token);
if (app == null) {
throw new AuthenticationException();
}
}
public void execute(SipServletRequest req, RemoteApplication app) throws IOException, ScriptException {
if (app instanceof ThriftApplication) {
try {
((ThriftApplication)app)._execute(req);
}
catch(TException e) {
removeApplication(app);
throw new IOException(e);
}
catch(AuthenticationException e) {
throw new ScriptException(e);
}
catch(SystemException e) {
throw new ScriptException(e);
}
catch(TropoException e) {
throw new ScriptException(e);
}
}
}
public void execute(HttpServletRequest req, RemoteApplication app) throws IOException, ScriptException {
if (app instanceof ThriftApplication) {
try {
((ThriftApplication)app)._execute(req);
}
catch(TException e) {
removeApplication(app);
throw new IOException(e);
}
catch(AuthenticationException e) {
throw new ScriptException(e);
}
catch(SystemException e) {
throw new ScriptException(e);
}
catch(TropoException e) {
throw new ScriptException(e);
}
}
}
class ApplicationCollector implements Runnable {
boolean _stop = false;
Thread _current;
public void run() {
_current = Thread.currentThread();
while(!_stop) {
Collection<RemoteApplication> dead = new LinkedList<RemoteApplication>();
for (RemoteApplication app : _index.values()) {
if (!app.isActive()) {
dead.add(app);
}
}
if (dead.size() > 0) {
for (RemoteApplication app : dead) {
LOG.info("Removing dead " + app);
app.dispose();
removeApplication(app);
}
}
try {
synchronized(this) {
this.wait(5000);
}
}
catch(InterruptedException e) {
//ignore
}
}
}
void stop() {
_stop = true;
_current.interrupt();
}
}
public void block(String key, String id, int timeout) throws AuthenticationException, TropoException, SystemException, TException {
RemoteApplication app = _index.get(key);
if (app != null && app instanceof ThriftApplication) {
((ThriftApplication)app).block(id, timeout);
}
else {
throw new AuthenticationException("Invalid key: " + key);
}
}
public AlertStruct call(String key, String from, String to, boolean answerOnMedia, int timeout)
throws AuthenticationException, TropoException, SystemException, TException {
return call_with_recording(key, from, to, answerOnMedia, timeout, null, null);
}
public AlertStruct call_with_recording(String key, String from, String to, boolean answerOnMedia, int timeout, String recordUri, String recordFormat) throws AuthenticationException, TropoException, SystemException, TException {
RemoteApplication app = _index.get(key);
if (app != null && app instanceof ThriftApplication) {
CallImpl call = ((ThriftApplication)app).call(from, to, answerOnMedia, timeout, recordUri, recordFormat);
return new AlertStruct(call.getId(), app.getApplicationID(), call.getCallerId(), call.getCallerName(), call.getCalledId(), call.getCalledName());
}
else {
throw new AuthenticationException("Invalid key: " + key);
}
}
public HangupStruct hangup(String key, String id) throws AuthenticationException, TropoException, SystemException, TException {
RemoteApplication app = _index.get(key);
if (app != null && app instanceof ThriftApplication) {
return ((ThriftApplication)app).hangup(id);
}
else {
throw new AuthenticationException("Invalid key: " + key);
}
}
public void log(String key, String id, String msg) throws AuthenticationException, TropoException, SystemException, TException {
RemoteApplication app = _index.get(key);
if (app != null && app instanceof ThriftApplication) {
((ThriftApplication)app).log(id, msg);
}
else {
throw new AuthenticationException("Invalid key: " + key);
}
}
public AlertStruct transfer(String key, String id, TransferStruct t) throws AuthenticationException, TropoException, SystemException, TException {
RemoteApplication app = _index.get(key);
if (app != null && app instanceof ThriftApplication) {
CallImpl call = ((ThriftApplication)app).transfer(id, t);
return new AlertStruct(call.getId(), app.getApplicationID(), call.getCallerId(), call.getCallerName(), call.getCalledId(), call.getCalledName());
}
else {
throw new AuthenticationException("Invalid key: " + key);
}
}
}