All Downloads are FREE. Search and download functionalities are using the official Maven repository.

sunlabs.brazil.asterisk.AsteriskHandler Maven / Gradle / Ivy

The newest version!
/*
 * AsteriskHandler.java
 *
 * Brazil project web application toolkit,
 * export version: 2.3 
 * Copyright (c) 2005-2006 Sun Microsystems, Inc.
 *
 * Sun Public License Notice
 *
 * The contents of this file are subject to the Sun Public License Version 
 * 1.0 (the "License"). You may not use this file except in compliance with 
 * the License. A copy of the License is included as the file "license.terms",
 * and also available at http://www.sun.com/
 * 
 * The Original Code is from:
 *    Brazil project web application toolkit release 2.3.
 * The Initial Developer of the Original Code is: suhler.
 * Portions created by suhler are Copyright (C) Sun Microsystems, Inc.
 * All Rights Reserved.
 * 
 * Contributor(s): suhler.
 *
 * Version:  1.17
 * Created by suhler on 05/05/25
 * Last modified by suhler on 06/11/13 10:23:07
 *
 * Version Histories:
 *
 * 1.17 06/11/13-10:23:07 (suhler)
 *   - add name mapping
 *   - better socket close handling
 *   - fix tag_ami bug
 *   .
 *
 * 1.16 06/08/02-14:04:46 (suhler)
 *   sync up with meetme server version.  Q expiration not yet added
 *
 * 1.15 06/06/02-17:09:18 (suhler)
 *   changes for registration timeouts (compiles, not tested)
 *
 * 1.14 06/04/25-14:54:48 (suhler)
 *   doc fixes
 *
 * 1.13 06/04/25-14:35:04 (suhler)
 *   add connection retries and retry intervals for asterisk servers that go away
 *
 * 1.12 06/02/07-14:01:50 (suhler)
 *   - make sure "Server" is added to every event
 *   - add "server" option to register, to restrict events to the particular server
 *
 * 1.11 06/02/06-15:43:34 (suhler)
 *   - add  to allow for synchronous AMI commands
 *   - wait a random time before the first keep-alive to keep the keep alives
 *   for multiple servers out of step
 *   - change lots of the diagnostic output to real log() messages
 *   - add more documentation
 *
 * 1.10 06/02/01-16:13:04 (suhler)
 *   - add keep-alive timer
 *   - clean up code and docs.
 *   - make sure we restart properly on socket errors
 *
 * 1.9 05/11/10-11:12:12 (suhler)
 *   version from pbx-support
 *
 * 1.8 05/11/10-10:56:17 (suhler)
 *   old cleanup
 *
 * 1.7 05/06/06-21:06:18 (suhler)
 *   add de-event stuff
 *
 * 1.6 05/05/31-12:16:52 (suhler)
 *   reorganize
 *
 * 1.5 05/05/31-10:11:37 (suhler)
 *   move event cache management into a separate method
 *
 * 1.4 05/05/31-08:55:01 (suhler)
 *   version from Frances
 *
 * 1.3 05/05/31-08:49:49 (suhler)
 *   checkpoint
 *
 * 1.2 05/05/27-10:22:31 (suhler)
 *   checkpoint.
 *
 * 1.2 05/05/25-23:57:34 (Codemgr)
 *   SunPro Code Manager data about conflicts, renames, etc...
 *   Name history : 1 0 asterisk/AsteriskHandler.java
 *
 * 1.1 05/05/25-23:57:33 (suhler)
 *   date and time created 05/05/25 23:57:33 by suhler
 *
 */

package sunlabs.brazil.asterisk;

import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Random;
import java.util.Vector;

import sunlabs.brazil.server.Handler;
import sunlabs.brazil.server.Request;
import sunlabs.brazil.server.Server;
import sunlabs.brazil.template.QueueTemplate.Queue;
import sunlabs.brazil.template.QueueTemplate.QueueItem;
import sunlabs.brazil.template.QueueTemplate;
import sunlabs.brazil.template.RewriteContext;
import sunlabs.brazil.template.Template;
import sunlabs.brazil.util.Format;
import sunlabs.brazil.util.StringMap;
import sunlabs.brazil.util.http.HttpInputStream;
import sunlabs.brazil.util.regexp.Regexp;

/**
 * Connect to asterisk manager api.  There is one connection
 * per server.  This is used both to issue Commands to Asterisk via
 * the manager interface, and to register for, and receive asynchronous
 * event notifications.
 * 

* Usage: *

 * <register queue=xxx key=xxx pattern=xxx [context=xxx server=xxx]>
 *  - register interest in a manager event (or events).  All generated events
 *    will be available with <dequeue name=xxx ...>, where the "name"
 *    parameter of the <dequeue> matches the "queue" parameter of
 *    <register>.
 * <unregister queue=xxx key=xxx>
 *  - unregister interest in a previously registered event.
 * <enqueue name="queue" from="my-q" ...>
 *  - Send an command to the manager interface. "queue" is the name
 *    specified in the handler "queue" parameter.  The enqueue'd data
 *    must have an "action" key.  The result of the command is obtained by:
 *    <dequeue name="my-q" ...>  where "my-q" is the "from" attribute of
 *    the corrosponding <enqueue>
 * <amicommand server=xxx action=xxx ...>
 *  - A syncronous version of the <enqueue> ... <dequeue> above.
 *    See below for details.
 * 
*

*/ public class AsteriskHandler extends Template implements Handler { static final String ID_DELIM = ":="; // Q identifier for ActionId static Events events = new Events(); // event rules go here /* * Keep track of the sever q names so we can do sanity checking * from the "amicommand" template */ static Vector servers = new Vector(); // all installed servers go here String queue = null; // name of the q to accept commands from /* * Various Events use inconsistent names for event parameters. * This table is used to map names to a consistent set. */ static Hashtable amiMap = new Hashtable(); static { amiMap.put("CallerIDname", "CallerIDName"); amiMap.put("CallerID", "CallerIDNum"); amiMap.put("CallerIDnum", "CallerIDNum"); } // the handler implementation starts here /** * Remember the host, port, id, and password for an asterisk * manager connection. We don't require that the asterisk * server actually be running at server startup time. We will try * hard to reconnect to the server if it goes away. *

*
queue *
The name of the Q to send manager commands to using the * <enqueue name="queue" ...>. * If not specifies, the "host:port" combination is used. *
server *
The server:port to use to contact the asterisk server *
userid, password *
The Manager credentials *
debug=true|false *
turn on more diagnostics on the console, depending on the current * server logging level. at "3", keep alives are logged, at "4", all events * are logged, and at "5" even more stuff is logged. *
keepalive=n *
If set, this handler will issue a keep-alive every "n" * seconds to the Asterisk Server. If the keep-alive fails, * a new connection will be attempted with the Asterisk server. *
retry=n *
Set the number of seconds to wait before retrying a broken * connection to an asterisk server (defaults to 10). *
* To communicate with multiple asterisk servers, each should have * their own handler instance. Servers are distinguished when sending * commands to them via the "queue" parameter, which should be * different for each server. Alternately, the <amicommand> server * parameter (which is the same as the server queue name) may be used. *

* When using multiple servers, only one of the server handler configurations * should be listed as a template (there can only be one template instance * for a given entity) which server doesn't matter, as events get registered * for all servers (the "server" attribute of the response determines * where it came from). As above, commands to a server are distinquished * with either the "queue" or "server" attributes, depending on whether * the command Qs are used directly, or the <amicommand> * template is used. *

To Do
* Figure out where to send unregistered events, such as "reload". */ public boolean init(Server server, String prefix) { String host = server.props.getProperty(prefix + "server"); queue = server.props.getProperty(prefix + "queue", host); String user = server.props.getProperty(prefix + "userid"); String pass = server.props.getProperty(prefix + "password"); boolean debug = Format.isTrue(server.props.getProperty(prefix+"debug")); int keepalive = 0; // send keepalives (seconds) try { keepalive = Integer.decode(server.props.getProperty(prefix + "keepalive")).intValue(); } catch (Exception e) {} if (host==null || user==null || pass==null) { server.log(Server.LOG_WARNING, prefix, "missing required parameter (server|userid|password)"); return false; } if (servers.contains(queue)) { server.log(Server.LOG_WARNING, prefix, "queue \"" + queue + "\" already in use!"); } else { servers.addElement(queue); } ConnectInfo conn = new ConnectInfo(host, user, pass); try { int sec = Integer.decode(server.props.getProperty(prefix + "retry")).intValue(); conn.setRetryTime(sec * 1000); } catch (Exception e) {} if (conn.verifyHost()) { AMIReader reader = new AMIReader(conn, queue, server, prefix); reader.setDebug(debug); reader.start(); if (keepalive > 0) { (new Keepalive(reader, keepalive)).start(); } return true; } else { server.log(Server.LOG_WARNING, prefix, "Unknown host: " + host); return false; } } /** * The handler only registers * servers. No requests are handled. * Use <register> in a template instead. */ public boolean respond(Request request) throws IOException { return false; } // The Template implementation starts here. /** * This only emits diagnostic information to stdout. */ public void tag_asterisk(RewriteContext hr) { debug(hr); hr.killToken(); String prefix = hr.get("prepend", hr.prefix + "event"); int i = 1; Enumeration e = events.getEvents(); while(e.hasMoreElements()) { hr.request.props.put(prefix + "." + (i++), ((EventItem) e.nextElement()).toString(";", ",")); } } /** * Issue a synchronous command to the Asterisk AMI interface. * This is a convenience for using <enqueue> * and <dequeue> directly. *

* Attributes: *

*
server
The Asterisk server's Q (required). *
action:
The action to perform. *
variable=value
one of the variables needed by the action. * (There is a fixed list, See the manager docs for more detail). *
timeout
The max time to wait for a response *
prepend
What to prepend all the results too. *
*/ public void tag_amicommand(RewriteContext hr) { debug(hr); hr.killToken(); String server = hr.get("server"); // Q of server to send command to String from = server + "-ami" + hr.sessionId; // where to get the answer int timeout=5; try { timeout = Integer.decode(hr.get("timeout")).intValue(); } catch (Exception e) {} String prepend = hr.get("prepend"); String action = hr.get("action"); if (prepend == null) { prepend = ""; } else if (!prepend.endsWith(".")) { prepend += "."; } if (action==null || server==null) { debug(hr, "action and server attributes are required"); hr.request.props.put(prepend + "error", "action or server missing"); return; } if (!servers.contains(server)) { debug(hr, "No such server running"); hr.request.props.put(prepend + "error", "invalid server"); return; } // copy the attributes into a new Map for the enqueue StringMap map = new StringMap(); Enumeration e = hr.keys(); while (e.hasMoreElements()) { String key = ((String) e.nextElement()).toLowerCase(); if (key.equals("timeout") ||key.equals("server") || key.equals("prepend")) { continue; } map.add(key, hr.get(key)); } /* * We could have an event from a previous cancelled request. * This can still break: we need to use ActionID matching * to get this right (XXX some day). */ QueueTemplate.getQ(from).clear(); boolean qd = QueueTemplate.enqueue(server, from, map, false, false); QueueTemplate.QueueItem qi = QueueTemplate.dequeue(from, timeout); if (qi == null) { hr.request.props.put(prepend + "error", "no data"); } else { StringMap result = (StringMap) qi.data; for (int i = 0; i < result.size(); i++) { hr.request.props.put(prepend + result.getKey(i), result.get(i)); } } } /** * Register an event. *
<register queue=xxx key=xxx exp=xxx [context=xxx server=xxx]>
     *  queue:  The Q name to send the results to.
     *  key:    The manager response key to match on.  Use "*" for all keys.
     *  exp:	A regular expression that matches a key value
     *  context: If specified, only events with this context are considered. 
     *  server: If specified, only events from this server are considered. 
     *	The server matches the "Server" item in the event, and is the server
     *  name, followed by a ":", then the port number (e.g. pbx.com:5038).
     * 
*/ public void tag_register(RewriteContext hr) { debug(hr); hr.killToken(); String queue = hr.get("queue"); String exp = hr.get("exp"); String key = hr.get("key", "*"); String context = hr.get("context"); String serverName = hr.get("server"); if (exp==null || queue==null) { debug(hr, "append tag needs queue and exp"); return; } addEvent(queue, key, exp, context, serverName); } /** * Java access to adding event registrations. */ public static void addEvent(String queue, String key, String exp, String context, String serverName) { events.addEvent(queue, key, exp, context, serverName); } /** * Unregister an event (or events). Match on "queue", "exp" and "key" */ public void tag_unregister(RewriteContext hr) { debug(hr); hr.killToken(); String queue = hr.get("queue"); String exp = hr.get("exp"); String key = hr.get("key"); if (key==null && exp==null && queue==null && !hr.isTrue("all")) { debug(hr, "Must specify all=true to remove all events"); } else { int removed = removeEvents(queue, key, exp); debug(hr, removed + " events removed"); } } /** * java access to removing event registrations. */ public static int removeEvents(String queue, String key, String exp) { return events.removeEvents(queue, key, exp); } static final int RETRY_CONNECTION_MS=10000; /** * Keep the info needed to connect and authenticate * to an asterisk AMI socket. */ static class ConnectInfo { public static final int AMI_PORT=5038; // asterisk manager port String host; // the asterisk host String user; // the user id String password; // the connection password int port=AMI_PORT; // the port AmiStringMap login; // the login map int retryMs; // how long to wait before retrying a dead conn. /** * Create the connection info * @param host asteriskserver[:port] */ public ConnectInfo(String host, String user, String password) { this.user=user; this.password=password; retryMs = RETRY_CONNECTION_MS; int indx = host.indexOf(":"); if (indx > 0) { try { port = Integer.decode(host.substring(indx+1)).intValue(); } catch (Exception e) {} this.host = host.substring(0, indx); } else { this.host=host; } login = null; } /** * Set the connection retry timer. */ public void setRetryTime(int ms) { retryMs = ms; } /** * Get the connection retry timer. */ public int getRetryMs() { System.out.println("Waiting " + (retryMs/1000) + "s ..."); return retryMs; } /** * Check to see if the server name exists. */ public boolean verifyHost() { try { InetAddress.getByName(host); return true; } catch (UnknownHostException e) { return false; } } /** * Try to get a socket to asterisk. */ public Socket connect() throws UnknownHostException, IOException { return new Socket(host, port); } /** * Return command required to login to the manager. */ public AmiStringMap loginMap() { if (login == null) { login = new AmiStringMap(); login.add("Action", "login"); login.add("Username", user); login.add("Secret", password); } return login; } /** * Identify the asterisk server we are talking to. */ public String getHost() { return host + ":" + port; } public String toString() { return "AMI connection to " + host + ":" + port + " as " + user; } } /** * Keep track of an event listener entry. [I'm not sure what this * should do yet.] Each time an event arrives, we traverse the list * checking for each regexp match. When a match is found, we send * the event to all the listening Q's. */ public static class EventItem { Vector queues; // The destination Q (or Q's) String name; // name of event for Q (string form of regexp) String key; // The key to match on String context; // the required context (if any) String serverName; // only this server (all if null) Regexp re; // Regular expression to match event public EventItem(String queue, String key, String exp, String context, String serverName) { name = exp; re = new Regexp(exp); this.key = key; this.context = context; this.serverName = serverName; queues = new Vector(); queues.addElement(queue); } /** * Add a new queue to an existing event. * @param queue The destination Q * @param exp The regular expression * @return true if there is now an event/q match */ public boolean addQ2Event(String queue, String key, String exp, String context, String serverName) { if (!exp.equals(name) || !key.equals(this.key)) { return false; } else if (this.context != null && !this.context.equals(context)) { return false; } else if (queues.indexOf(queue)<0) { queues.addElement(queue); } return true; } /** * Remove an exp/Q pair. Return true if removed. * @param queue The destination Q to remove (or all if null) * @param key The event key to match on (null for all keys) * @param exp The event re (or null for all re's) * @return true if something was removed */ public boolean remQEvent(String queue, String key, String exp) { if ((exp==null || exp.equals(name)) && (key==null || key.equals(this.key))) { int indx; if (queue == null && queues.size() > 0) { // System.out.println("RMQ Clearing " + this); queues.clear(); return true; } else if ((indx = queues.indexOf(queue)) >= 0) { queues.removeElementAt(indx); // System.out.println("RMQ Removing " + queue + " from " + this); return true; } } return false; } public int size() { return queues.size(); } /** * Send an event to the q's if there is a match. * XXX need to think about event format. * XXX if key contains '*' or '?' do globbing */ public boolean send2Q(Dictionary event) { String value = (String) event.get(key); String context = (String) event.get("context"); String sn = (String) event.get("Server"); if (value != null && re.match(value) != null && (this.context==null || context.equals(context)) && (this.serverName==null || serverName.equals(sn))) { for(int i=0;i" + queues; } /** Machine readable version */ public String toString(String delim, String delim2) { String result = name + delim + key + delim + context + delim + serverName + delim; String d2 = ""; for (int i=0;i * Much of the useful information in a response is implied from * previous responses with the same "uniqueid" field. We accumulate * of all the data associated with a "uniqueid", and send the accumulation * when needed. *

* If our connection to Asterisk fails, we kill the corrosponding * Writer thread, and attempt to re-establish the connection. */ static class AMIReader extends Thread { ConnectInfo info; // info required to connect to server String queue; // name of Q to get command from String prefix; // name of our prefix in server props Server server; // a server reference for logging Writer writer = null; // thread to send commands to * boolean debug=false; // log stuff to stderr? Socket sock = null; // socket to server HttpInputStream in; // the input stream to read from // keep pending events for this server // keys are unique id's // values are the event StringMaps Hashtable cache = new Hashtable(); public AMIReader(ConnectInfo info, String queue, Server server, String prefix) { this.info = info; this.queue = queue; this.server=server; this.prefix=prefix; setName("asterisk listener: " + queue); setDaemon(true); } public String getQueueName() { return queue; } public String getHost() { return info.getHost(); } /** * Connect to the * server if we aren't connected to it. * Then login to it. * XXX this needs to work with Writer better, and not * send the login commands directly. */ boolean verifyConnection() { Request.HttpOutputStream out; // where to write commands to String loginStr = info.loginMap().commandify(null); if (sock==null) { try { sock = info.connect(); in = new HttpInputStream(sock.getInputStream()); out = new Request.HttpOutputStream(sock.getOutputStream()); out.writeBytes(loginStr); out.flush(); } catch (IOException e) { sock=null; if (writer != null) { writer.die("failed to create socket"); writer=null; } server.log(Server.LOG_WARNING, "asterisk", "Can't connect to " + this + " " + e); return false; } if (queue != null) { writer = new Writer(queue, out, this); writer.start(); } else { server.log(Server.LOG_LOG, "asterisk", "No command queue listener defined"); } return true; } // XXX we should check the result here return true; } public void run() { log(3,"Starting thread: " + info); // We'll reuse this map for each event, so we need to // give away copies. // AmiStringMap map = new AmiStringMap(); while(true) { if (!verifyConnection()) { log(2,"Connection didn't restart, wait: " + info); try { Thread.sleep(info.getRetryMs()); } catch (InterruptedException e) { log(5, (info.getRetryMs()/1000) + "s sleep interrupted: " + e); } continue; } map.clear(); try { map.read(in); } catch (IOException e) { log(3,"read from asterisk failed: " + e); try { Thread.sleep(info.getRetryMs()); } catch (InterruptedException e2) { log(5, (info.getRetryMs()/1000) + "s sleep interrupted: " + e2); } map.clear(); close(); cache.clear(); continue; } String type = "error"; try { type = map.getKey(0).toLowerCase(); } catch (Exception e) { log(3,"bad event type: " + map); } map.add("Server", getHost()); if (type.equals("event")) { AmiStringMap merged = getMergedMap(map); boolean sent = events.processEvents(merged); log(4,"Event: " + (sent ? "(SENT) ":"") + map); } else if (type.equals("response") && writer!= null) { AmiStringMap reply = new AmiStringMap(map); log(4,"Command response: " + reply); writer.reply(reply, queue); } else { log(3, "Unknown Asterisk AMI reponse: " + type); } } } /** * force the socket closed, so we can reconnect */ public void close() { if (sock != null) { try { sock.close(); } catch (Exception e) { log(2, "Asterisk reader socket won't close: " + e); } sock = null; } if (writer != null) { writer.die("socket went away"); writer=null; } } /** * Merge event with previous events and return the union. This * manages the cache of saved events. * @param map The current event * @return The original map or a "merged" map */ AmiStringMap getMergedMap(AmiStringMap map) { String id = map.get("uniqueid"); if (id == null) { log(5,"No unique-id for " + map); return new AmiStringMap(map); // return map; } AmiStringMap cached = (AmiStringMap) cache.get(id); if (cached == null) { log(5, "caching info for unique-id " + id); cached = new AmiStringMap(); cached.append((AmiStringMap)map, true); cache.put(id, cached); } else { cached.append(map, false); } if ("Hangup".equals(map.get("event"))) { cache.remove("id"); log(5, "Removing uniquid cache: " + id); } return new AmiStringMap(cached); } /** * Read command from the command Q, and send them to the AMI. * We need a different thread to read commands from the command Q, * and write them to the socket, so we won't block. It will * be managed by our reader thread. */ static class Writer extends Thread { String queue; // q to read commands from AMIReader reader; Request.HttpOutputStream out = null; // where to write commands to boolean alive=true; Writer(String queue, Request.HttpOutputStream out, AMIReader reader) { this.queue = queue; this.out = out; this.reader = reader; setName("asterisk writer: " + queue); setDaemon(true); } /* * Loop reading commands from the Q and sending them to AMI. * if our AMI connection dies, then end this thread; the * reader will detect the error and make a new one of us. */ public void run() { System.out.println("Running command thread for: " + queue); while(alive) { QueueTemplate.QueueItem item = QueueTemplate.dequeue(queue, 10000); if (item == null || !alive) { continue; } StringMap data = (StringMap) item.data; reader.log(5, "AMI command from " + item.from + ": " + data); String cmd = AmiStringMap.commandify(data, item.from); if (cmd == null) { System.out.println("Bad command"); continue; } try { out.writeBytes(cmd); out.flush(); } catch (IOException e) { System.out.println("Command write failed"); // our reader will pick up this error and // call die() } } System.out.println("Killed command thread for: " + queue); } /* * Extract the Q name out of the "ActionID". * We need to match the manager commands with their responses, * and then send the response to the proper Q. We'll tack * the Q name onto the end of the ActionId, and strip it off * when the result comes back. */ String getId(StringMap data) { String id = data.get("actionid"); int index; if (id == null || (index=id.lastIndexOf(ID_DELIM)) < 0) { System.err.println("Warning! no ID in ActionID"); return null; } data.remove("actionid"); if (index > 0) { data.put("ActionId", id.substring(0, index)); } return id.substring(index+ID_DELIM.length()); } /** * We got a response to a command. Deal with it. */ public void reply(AmiStringMap map, String from) { String queue = getId(map); if (queue != null) { QueueTemplate.enqueue(queue, from, map, false, false); } else { // XXX need to check for authentication failed! System.out.println("No Q name in ActionID! " + map); } } /** * Cause this thread to die. The interrupt * will terminate the wait in dequeue */ void die(String why) { System.out.println("Die!!: " + why); alive=false; interrupt(); } } public void setDebug(boolean debug) { this.debug = debug; } public void log(int level, String s) { if (debug) { server.log(level, prefix, s); } } } /** * Class to manage the set of events. This implementation * maintains a vector of eventItems. * * XXX We need to detect when the requester of an event goes away * XXX without unregistering the event, so we can remove it for them. */ public static class Events { Vector events; public Events () { events = new Vector(); } /** * Add an event to the current set of events. * If the event expression already exists, add the queue name * to the existing event, otherwise create a new event item. */ public void addEvent(String queue, String key, String exp, String context, String serverName) { boolean added = false; for(int i=0;i * AMI responses are either: *

  • *
      Mime-style headers, followed by a blank line, or *
        The header "Response: follows", followed by zero or more * additional headers, followed by one or more * lines of output, followed by the line: *
        --END COMMAND--
        * Unfortunately, the first line of the following response can have a * ":" in it, making it indistinguishable from another header [they * should'a added a blank line after the last header]. We need to use * some heuristics to figure out if it's a header or data. grumph! *

        XXX to do:
        * Any time data follows, the "ActionID" key (if present) will always * be the last key before the data starts. We could use that, or if * the data consists of what looks like headers, then just make them * headers, and don't stuff them into "data", which is sort-of what * happens now. *

      * In the second case, all the response data is put in a header called: * data: *

      * This is modelled after MimeHeaders. */ public static class AmiStringMap extends StringMap { public AmiStringMap() {} public AmiStringMap(StringMap map) { append(map, true); } public void read(HttpInputStream in) throws IOException { boolean hasContent=false; // do we have additional content? StringBuffer sb = null; while(true) { String line = in.readLine(); // System.out.println("[" + line + "]"); if (line == null) { throw new IOException("Null read from Asterisk"); } if (sb != null && line.trim().equals("--END COMMAND--")) { in.readLine(); // strip trailing NL break; } else if (sb != null) { sb.append(line).append("\n"); } else if (line.length() == 0) { // end of headers break; } else { // name: value header int index = line.indexOf(":"); String key; if (index > 0 && (key = line.substring(0,index).trim()).indexOf(" ") < 0) { // key/value pair String value = line.substring(index+1).trim(); if (key.toLowerCase().equals("response") && value.toLowerCase().equals("follows")) { hasContent=true; } add(key, value); // actionid is the last header! if (hasContent&&key.toLowerCase().equals("actionid")) { sb = new StringBuffer(); } } else if (hasContent && size() > 0) { sb = new StringBuffer(line).append('\n'); } else if (size() > 0) { // continuation line? String value = get(size()-1); put(size()-1, value + "\r\n\t" + line.trim()); } } } if (sb != null) { put("data", sb.toString()); } } /** * Turn an AmiMap into an asterisk command. * Make sure the "Action" keyword exists and is first. * @param: id add the "id" at the end of the actionid * @return null if there wasn't a valid command */ public String commandify(String id) { return commandify(this, id); } public static String commandify(StringMap map, String id) { String action = map.get("action"); if (action == null) { return null; } String existing = ""; // existing action id StringBuffer sb = new StringBuffer("Action: "); sb.append(action).append("\r\n"); for (int i = 0; i < map.size(); i++) { String key = map.getKey(i); if (key.toLowerCase().equals("action")) { continue; } if (id != null && key.toLowerCase().equals("actionid")) { existing=map.get(i); continue; } sb.append(key).append(": "); sb.append(map.get(i)).append("\r\n"); } if (id != null) { sb.append("ActionId: " + existing + ID_DELIM + id); sb.append("\r\n"); } sb.append("\r\n"); return sb.toString(); } } // wait a random amount for the first keepalive static Random rand = new Random(); /** * Thread to issue keep-alives to our asterisk connection. * Every "interval" seconds, it issues a "ping" and waits for * a "pong". If it doesn't get a response, it causes the connection * to Asterisk to be restarted. */ static class Keepalive extends Thread { boolean first = true; int interval; // keep alive interval AMIReader reader; // thread reading events from Asterisk String to; // name of sending Q StringMap ping = new StringMap(); Keepalive(AMIReader reader, int interval) { this.reader = reader; if (interval < 5) interval=5; this.interval = interval * 1000; to = reader.getQueueName(); setName("keepalive-" + to); setDaemon(true); ping.put("Action", "Ping"); } public void run() { reader.log(3,"Starting keepalive thread"); String from = to + "ping"; while(true) { try { if (first) { Thread.sleep(1000 + Math.abs(rand.nextInt())%interval); first = false; } else { Thread.sleep(interval); } } catch (Exception e) { reader.log(3,"Keepalive interrupted: " + e); } QueueTemplate.enqueue(to, from, ping, false, false); QueueTemplate.QueueItem item=QueueTemplate.dequeue(from, 5); // timeout, try to restart the connection if (item == null) { reader.log(2,reader.getHost() + ": keepalive failed!"); // reader.interrupt(); reader.close(); } else { reader.log(3,reader.getHost() + " is alive!"); } } } } }





  • © 2015 - 2024 Weber Informatics LLC | Privacy Policy