Posted
almost 17 years
ago
From my participation in the asterisk-users and asterisk-java-users mailing lists, and from the general questions I see, I think a common use case for the combination of the Gateway interface and the Manager interface is to deliver outbound
... [More]
recorded messages. I think that an example might help to show how the two interfaces (and the two parts of Asterisk-Java) can be leveraged to deliver outbound messages.
However, because I've recently wanted to write more complex applications in Scala (a language interoperable with Java), I'm going to show you how to perform outbound message delivery with Scala and Asterisk-Java and without modification of the dialplan. If you liked my example of Scala and want to see more, I'd recommend this series if you have a Java background.
Here's the plan for our simple application:
Design an Agi script that plays back our message
Create & start an AgiServer object to host our script
Create a Manager connection to Asterisk
Using the Manager connection, ask Asterisk to originate a call from some destination to our Agi script
Create a callback handler to act if/when the originate fails, succeeds, or we get disconnected
First, here's some definitions and imports we'll need in order to use Console (the System.out analog) and the Asterisk-Java library:
package org.asteriskjava.blog.scala
import actors.Actor
import Console.println
import org.asteriskjava.fastagi._
import org.asteriskjava.live._
Next, here's the simple Agi script singleton class that simply plays the hello-world sound file, and then counts to ten. This is what our users will hear as the message.
object OutgoingMessageScript extends BaseAgiScript
{
def service(request: AgiRequest, channel: AgiChannel): Unit = {
streamFile("hello-world")
for(i <- 0.until(10))
sayNumber((i 1) "")
}
}
Skipping ahead of a few steps (because it doesn't depend on anything else), here's the class that implements the callback interface, providing instructions on what to do if the originate dials, fails, succeeds, it's busy or not answered, or we get disconnected. This handler simply prints out what happened, though in your own code, this would be a good place to queue up something that failed, or mark it as a preliminary success (I say preliminary because this only means the originate succeeded, but not the playback... yet).
object OutgoingMessageStatusReport extends OriginateCallback
{
def onDialing(channel: AsteriskChannel): Unit = println("Dialing-" channel)
def onSuccess(channel: AsteriskChannel): Unit = println("Success-" channel)
def onNoAnswer(channel: AsteriskChannel): Unit = println("No Answer-" channel)
def onBusy(channel: AsteriskChannel): Unit = println("Busy-" channel)
def onFailure(cause: LiveException): Unit = println("Failure-" cause)
}
And finally, here's the start of the main class and method...
object OutgoingMessageDelivery {
def main(args : Array[String]) : Unit = {
We'll create an AGI server, and use Actor to get the startup method executed in a separate thread. We do this so that it will begin to accept connections without blocking our current thread.
// start an AGI server with the given script
val asteriskGatewayServer = new DefaultAgiServer(OutgoingMessageScript)
new Actor { def act = asteriskGatewayServer.startup }.start
Next, we'll connect to Asterisk through the Manager interface too (via the nice Live API that wraps it).
// start a connection to the manager interface
val asteriskManagerInterface = new DefaultAsteriskServer(
"192.168.1.15",
"root",
"secret")
Finally, we will ask through the Manager interface that Asterisk originate to "SIP/xlite1" (if you were doing massive outbound delivery with T1/E1 circuits, you'd probably put something like "Zap/r1/outbound number" here)
// connect some channel to the script, with a callback
asteriskManagerInterface.originateToApplicationAsync(
"SIP/xlite1", // **** could be "Zap/r1/1234567890"
"Agi", "agi://" java.net.InetAddress.getLocalHost.getHostAddress,
10*1000,
OutgoingMessageStatusReport)
}
}
And that's it! Asterisk will try to originate that call, and inform us (via the callback) of the outcome of the attempted originate.
Here's what the log will look like:
2008-08-09 16:02:04,671 org.asteriskjava.fastagi.DefaultAgiServer INFO [Thread-1] - Listening on *:4573.
2008-08-09 16:02:04,718 org.asteriskjava.manager.internal.ManagerConnectionImpl INFO [main] - Connecting to 192.168.1.15:5038
2008-08-09 16:02:04,843 org.asteriskjava.manager.internal.ManagerConnectionImpl INFO [Asterisk-Java ManagerConnection-0-Reader-0] - Connected via Asterisk Call Manager/1.0
2008-08-09 16:02:04,859 org.asteriskjava.manager.internal.ManagerConnectionImpl INFO [main] - Successfully logged in
2008-08-09 16:02:04,875 org.asteriskjava.manager.internal.ManagerConnectionImpl INFO [main] - Determined Asterisk version: Asterisk 1.4
2008-08-09 16:02:04,890 org.asteriskjava.live.internal.AsteriskServerImpl INFO [main] - Initializing done
2008-08-09 16:02:04,937 org.asteriskjava.live.internal.ChannelManager INFO [Asterisk-Java DaemonPool-1-thread-1] - Adding channel SIP/xlite1-081da108(1218312125.13)
Dialing-AsteriskChannel[id='1218312125.13',name='SIP/xlite1-081da108',callerId='',state='DOWN',account='null',dateOfCreation=Sat Aug 09 16:02:04 EDT 2008,dialedChannel=null,dialingChannel=null,linkedChannel=null]
2008-08-09 16:02:06,218 org.asteriskjava.fastagi.DefaultAgiServer INFO [Thread-1] - Received connection from /192.168.1.15
Success-AsteriskChannel[id='1218312125.13',name='SIP/xlite1-081da108',callerId='',state='UP',account='null',dateOfCreation=Sat Aug 09 16:02:04 EDT 2008,dialedChannel=null,dialingChannel=null,linkedChannel=null]
2008-08-09 16:02:06,218 org.asteriskjava.fastagi.DefaultAgiServer INFO [Thread-1] - Thread pool started.
2008-08-09 16:02:06,375 org.asteriskjava.fastagi.internal.FastAgiConnectionHandler INFO [Asterisk-Java DaemonPool-2-thread-1] - Begin AgiScript org.asteriskjava.blog.scala.OutgoingMessageScript$ on Asterisk-Java DaemonPool-2-thread-1
Questions? Post on our mailing list or reply to the post!
It's worth nothing, too, that there are certainly already ways of doing this that may only involve the dialplan. Also, this doesn't solve the problem of detecting answering machines.
The complete source:
package org.asteriskjava.blog.scala
import actors.Actor
import Console.println
import org.asteriskjava.fastagi._
import org.asteriskjava.live._
object OutgoingMessageDelivery {
def main(args : Array[String]) : Unit = {
// start an AGI server with the given script
val asteriskGatewayServer = new DefaultAgiServer(OutgoingMessageScript)
new Actor { def act = asteriskGatewayServer.startup }.start
// start a connection to the manager interface
val asteriskManagerInterface = new DefaultAsteriskServer(
"192.168.1.15",
"root",
"secret")
// connect some channel to the script, with a callback
asteriskManagerInterface.originateToApplicationAsync(
"SIP/xlite1", // **** could be "Zap/r1/1234567890"
"Agi", "agi://" java.net.InetAddress.getLocalHost.getHostAddress,
10*1000,
OutgoingMessageStatusReport)
}
}
object OutgoingMessageScript extends BaseAgiScript
{
def service(request: AgiRequest, channel: AgiChannel): Unit = {
streamFile("hello-world")
for(i <- 0.until(10))
sayNumber((i 1) "")
}
}
object OutgoingMessageStatusReport extends OriginateCallback
{
def onDialing(channel: AsteriskChannel): Unit = println("Dialing-" channel)
def onSuccess(channel: AsteriskChannel): Unit = println("Success-" channel)
def onNoAnswer(channel: AsteriskChannel): Unit = println("No Answer-" channel)
def onBusy(channel: AsteriskChannel): Unit = println("Busy-" channel)
def onFailure(cause: LiveException): Unit = println("Failure-" cause)
}
[Less]
|
Posted
almost 17 years
ago
Chris Heller has published a series of two blog posts on how to integrate Oracle's PeopleSoft with Asterisk using Asterisk-Java.
In PeopleSoft IVR Integration the easy way he explains how to "initiate a phone call to the end user from PeopleSoft
... [More]
, prompt them for a PIN code, and take action in PeopleSoft depending on whether they were successful or not. This may be used as part of the initial signon process for two-factor authentication of your PeopleSoft users, or you might tie this in with some business logic (e.g. be really sure who is sending off a large wire transfer)."
Part 2 goes into more detail on how use the integration for
two factor authentication with user events.
References
PeopleSoft IVR Integration the easy way (Part 1)
PeopleSoft Telephony Two Factor Authentication (Part 2)
[Less]
|
Posted
almost 17 years
ago
Every year, O'Reilly hosts an open-source conference in Porland, OR, USA, in July. If anyone interested is at the conference, I'll be at Mark Spencer's talk on Thursday at 1:45pm. Stop by and say hello -- I'll be wearing the conference badge
... [More]
with my name on it. I'm always delighted to meet other like-minded Asterisk and Asterisk-Java users.
[Less]
|
Posted
about 17 years
ago
A few weeks ago, Matt Gibson posted on the asterisk-users discussion list that he was looking for a GraphViz script he had heard about a few years ago that could generate graphs of Asterisk's extensions.conf file that defines the dialplan. At
... [More]
the time, I had been working on a recently open-sourced application, EgoNet, for a researcher I work with. This application taught me much about social networks, particularly his specialty of egocentric networks, and it also helped me learn a ton about the Java Universal Network/Graph Framework (or JUNG). I immediately thought about combining my experience with JUNG and Asterisk, and wrote a rudimentary (read as: extremely ugly) parser for extensions.conf and generated a crude graph of the sample dialplan.
Here's a picture of it:
I posted my image back to the list and got many interesting responses. Given all of the interest, and remembering that Stefan had recently added some config file parsing to Asterisk-Java for management of the configuration file format Asterisk uses, I decided to add extensions.conf-specific parsing to his original work.
So far, I've only written in some very, very simple grouping of the different kinds extensions.conf statements, specifically support for exten and include. I've also added the parsing of the exten line to recognize the extension name, priority, application, and arguments. Because it is the authoritative source for all of this is pbx_config.c, I couldn't resort to flex or bison. At the moment, I haven't differentiated any further into interpreting the dialplan, as I don't really care about things like priorities like same or n 1 or anything other than basic structure.
Without further ado, I'd like to show some of what can be done with the dialplan specific parsing I've done, and give some examples, including a Java Web Start demo (so get your extensions.conf file ready!).
Example 0: Just printing out the parsed information
Initially, I just wanted a trivial example to print out what I had parsed. Here, I print out every context and its extensions. Below is the code, and then the output. I trimmed the output to show what new classes I've created outside of Stefan's original config package. The important ones are ConfigInclude and ConfigExtension, as those will give us some ties for our graph.
Show/Hide Code and Output for Example 0
Code for Example 0:
public class GraphExample0
{
public static void main(String [] args) throws Exception
{
ExtensionsConfigFile configFile = readFile();
for(Category ctx : configFile.getContexts())
{
System.out.println(ctx.getClass().getSimpleName() ": " ctx " (base " ctx.getBaseCategories() ") --");
for(ConfigElement el : ctx.getElements())
System.out.println("\t" el.getClass().getSimpleName() ": " el);
}
}
public static ExtensionsConfigFile readFile() throws Exception
{
String fileName = "./extensions.conf.bebr";
ExtensionsConfigFileReader fileReader = new ExtensionsConfigFileReader();
ExtensionsConfigFile configFile = fileReader.readExtensionsFile(fileName);
return configFile;
}
}
Output for Example 0:
Category: general (base []) --
ConfigVariable: static=yes
ConfigVariable: writeprotect=no
ConfigVariable: clearglobalvars=no
Category: macro-dundi-e164 (base []) --
ConfigExtension: exten => s,1,[Goto, ${ARG1}, 1]
ConfigInclude: include => dundi-e164-lookup
Examples 1 & 2: Initial network graph definition
The next step was to decide what to graph. Because contexts are the major structural feature in a dialplan, I chose to make contexts the nodes. Next, I needed to decide what ties contexts together in order to have some edges in my graph. I chose include statements so that my graph would show the overall structure of the dialplan. You could choose anything for the ties, and later on, I throw in Goto statements as well. As you can see from the code examples, I had to add two new methods -- one for figuring out the nodes (buildGraph) and one for figuring the edges around that node (getIncludesContext). After looking at the output, I decided I needed labels on my nodes and edges. That's example 2, which I haven't shown in code here (it's included in the download at the end of the article). Finally, for example 2, I created a class, SimpleGraphViewer that handled the graph part of things here. It is based directly from the ShowLayouts demo in JUNG.
Show/Hide Code for Examples 1 & 2
public static void main(String[] args) throws Exception
{
ExtensionsConfigFile configFile = readFile();
Graph graph = buildGraph(configFile);
Layout layout = new KKLayout(graph);
VisualizationViewer vv = new VisualizationViewer(layout);
JPanel jp = new JPanel();
jp.setBackground(Color.WHITE);
jp.setLayout(new BorderLayout());
jp.add(vv, BorderLayout.CENTER);
JFrame frame = new JFrame();
frame.add(vv);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.pack();
frame.setVisible(true);
}
static Graph buildGraph(ExtensionsConfigFile configFile)
{
Graph graph = new DirectedSparseGraph();
for (Category c1 : configFile.getContexts())
{
for (Map.Entry entry : getIncludesContext(c1, configFile.getContexts()).entrySet())
{
Category c2 = entry.getValue();
System.out.println("adding " entry.getKey());
graph.addEdge(entry.getKey(), c1, c2, EdgeType.DIRECTED);
}
}
return graph;
}
/**
* Given an unchanging list of all possible config categories, and an origin
* category, find the list of all destination categories; a destination is
* defined as an include statement for this example.
*
* @param originatingContext the context for which you want to find destinations
* @param fullList the full list of contexts, after parsing has completed
* @return all destination contexts mapped by the config directive that references them
*/
public static Map getIncludesContext(Category originatingContext, Collection fullList)
{
Map dest = new HashMap();
for(ConfigElement e : originatingContext.getElements())
{
if(!(e instanceof ConfigInclude))
continue;
ConfigInclude searchingContext = ((ConfigInclude)e);
for(Category candidateContext : fullList)
{
if(originatingContext == candidateContext)
continue;
if(!searchingContext.getName().trim().toLowerCase().equals(candidateContext.getName().trim().toLowerCase()))
continue;
dest.put(searchingContext, candidateContext);
}
}
return dest;
}
Here's the output graphs of examples 1 and 2:
Example 3: Adding even more to the graph
After I saw example 2, I wanted to add Goto() statements as edge ties to other context nodes and I wanted to size nodes by how many extensions they had. I also wanted to clean up all of the labeling. I used JUNG's normal ways of doing both of these, which you may follow along with in the code samples. These graphs are starting to look really neat.
Show/Hide Code for Example 3
// change this to xyzzy to keep macros in the graph
static final String ignorePrefix = "macro-";
static Graph buildGraph2(ExtensionsConfigFile configFile)
{
Graph graph = new DirectedSparseMultigraph();
for (Category c1 : configFile.getContexts())
{
if(c1.getName().toLowerCase().startsWith(ignorePrefix))
continue;
for (Map.Entry entry : getIncludesContext(c1, configFile.getContexts()).entrySet())
{
Category c2 = entry.getValue();
if(c2.getName().toLowerCase().startsWith(ignorePrefix))
continue;
if(!graph.containsEdge(entry.getKey()))
{
System.out.println("adding " entry.getKey());
graph.addEdge(entry.getKey(), c1, c2, EdgeType.DIRECTED);
}
}
for (Map.Entry entry : getGotoContext(c1, configFile.getContexts()).entrySet())
{
Category c2 = entry.getValue();
if(c2.getName().toLowerCase().startsWith(ignorePrefix))
continue;
if(!graph.containsEdge(entry.getKey()))
{
System.out.println("adding " entry.getKey());
graph.addEdge(entry.getKey(), c1, c2, EdgeType.DIRECTED);
}
}
}
return graph;
}
public static Map getGotoContext(Category originatingContext, Collection fullList)
{
Map dest = new HashMap();
for(ConfigElement e : originatingContext.getElements())
{
if(!(e instanceof ConfigExtension))
continue;
ConfigExtension searchingContext = ((ConfigExtension)e);
String [] app = searchingContext.getApplication();
// skip everything but goto and gotoif
if(!(app.length == 4 && app[0].toLowerCase().startsWith("goto")))
continue;
String searchingContextString = app[1];
for(Category candidateContext : fullList)
{
if(originatingContext == candidateContext)
continue;
if(!searchingContextString.trim().toLowerCase().equals(candidateContext.getName().trim().toLowerCase()))
continue;
dest.put(searchingContext, candidateContext);
}
}
return dest;
}
public static void main(String[] args) throws Exception
{
ExtensionsConfigFile configFile = readFile();
Graph graph = buildGraph2(configFile);
Layout layout = new KKLayout(graph);
VisualizationViewer vv = new VisualizationViewer(layout);
vv.getRenderContext().setVertexLabelTransformer(new CategoryNameLabeller());
vv.getRenderContext().setVertexShapeTransformer(new CategorySizeAspect());
vv.getRenderContext().setEdgeDrawPaintTransformer(new EdgePaintSelector(vv));
new SimpleGraphViewer(vv).setVisible(true);
}
public static class CategorySizeAspect
extends AbstractVertexShapeTransformer
implements Transformer
{
int maxEdges = 0;
public CategorySizeAspect()
{
super();
setSizeTransformer(new Transformer() {
public Integer transform(Category v) {
return (int)(v.getElements().size()) 25;
}});
}
@Override
public Shape transform(Category cat)
{
return factory.getEllipse(cat);
}
}
public static class EdgePaintSelector
implements Transformer
{
GradientEdgePaintTransformer includesEdge;
GradientEdgePaintTransformer gotoEdge;
GradientEdgePaintTransformer unknownEdge;
public EdgePaintSelector(VisualizationViewer vv)
{
includesEdge = new GradientEdgePaintTransformer(Color.blue.darker().darker(),Color.blue,vv);
gotoEdge = new GradientEdgePaintTransformer(Color.green.darker().darker(),Color.green,vv);
unknownEdge = new GradientEdgePaintTransformer(Color.white,Color.white,vv);
}
@Override
public Paint transform(ConfigElement e)
{
if(e instanceof ConfigInclude)
return includesEdge.transform(e);
else if(e instanceof ConfigExtension)
return gotoEdge.transform(e);
return unknownEdge.transform(e);
}
}
public static ExtensionsConfigFile readFile2() throws Exception
{
JFileChooser jfc = new JFileChooser();
int result = jfc.showOpenDialog(null);
if(result != JFileChooser.APPROVE_OPTION)
return new ExtensionsConfigFile("",new HashMap());
File file = jfc.getSelectedFile();
String fileName = file.getPath();
ExtensionsConfigFileReader fileReader = new ExtensionsConfigFileReader();
ExtensionsConfigFile configFile = fileReader.readExtensionsFile(fileName);
return configFile;
}
Here's the output of the extensions.conf that comes default with Asterisk, as well as output from a more complex dialplan I picked up along the way
Try it on your own extensions.conf
or skip ahead to download the JAR file containing the source.
Please note: The web start demo requires you to accept a self-signed certificate as web start won't allow the demo to read the local extensions.conf file you select! If you're worried about this, feel free to download and build the source yourself.
Here's what the demo looks like with my extensions.conf and JUNG's ISOMLayout. The demo contains two different mouse modes for interacting with the graph as well as a button to save the image. If the demo doesn't work, or you encounter any trouble or things that need to be corrected, feel free to reply and let me know. Thanks!
[Less]
|
Posted
about 17 years
ago
A recent post to the mailing list asked about how to stop and start a FastAGI server and how folks host Fast AGI servers. I shared some code I wrote for a small MBean that starts and stops a DefaultAgiServer from Asterisk-Java -- see the last FAQ
... [More]
question "Can I have an example of using an AGI server in a container like JBoss?". The MBean uses the JBoss-specific @Management annotation.
I work on a home-grown call center management application (EJBs, JSP webpages, and two standalone Java Swing applications, all talking through JBoss Application Server), and we've already been able to add a couple features with Asterisk-Java that add value and utility to our application. I didn't want to introduce another service to our infrastructure and I also didn't want to write an entirely new Agi server, as Stefan had done a great job on that part. Thus, the above MBean was born.
Comments and suggestions and other examples of how you run your Agi scripts are welcome and encouraged on our mailing lists.
[Less]
|
Posted
about 17 years
ago
Atlassian has improved the charting features in their latest release of Fisheye. So I had a look at the Asterisk-Java code base to see how it's major packages compare in size. This is what i got:
Let's have a look at the individual packages:
... [More]
The Manager API
Support for the Manager API (manager) is by far the biggest package in Asterisk-Java. It provides access to a variety of Asterisk features from call control to monitoring and call center management. The size of this package and its steady groth ressemble the groth of features exposed by Asterisk.
The Asterisk Gateway Interface
FastAGI is an easy way to implement IVR applications in Java. The feature set is quite stable over time so we don't see significant changes in the code base. You'll even notice that support for AsyncAGI (i.e. running AGI scripts over the Manager API) was a quick win as it didn't have a significant impact on the overall size of the package.
Parsing Configuration Files
The config package is quite new and supports parsing Asterisk's configuration files. All config files use the same syntax and group up configuration items into contexts. It supports inheritance of contexts, includes and forms the basis for further development like configuration editors or dialplan visualization (stay tuned for more information from Martin on this topic).
Originally developed to parse voicemail meta files the config package is certainly a candidate for future growth.
The Live API
The live API is our own invention and takes integration a step further. Instead of only providing access to the basic features exposed by Asterisk the live API offers real objects with state and behavior that model core concepts found in Asterisk. There are channel objects with state like the current caller id, the progress or hangup reason and methods to redirect channels, start and stop monitoring and much more.
The live API speeds up developers by hiding the sometimes tedious details of the low level Asterisk APIs and has reached a significant size. Next to the config package the live API is under active development and will certainly grow in the future.
[Less]
|
Posted
about 17 years
ago
Mauro has published an interesting article about Asterisk and SugarCRM integration. A swing popup notifies its user on incoming calls and provides additional information about the customer that is retrieved from SugarCRM.
The sample application
... [More]
serves as a good example on how to use Asterisk's Manager API with Asterisk-Java.
References
Mauro: SugarCRM and Asterisk integration in Java
[Less]
|
Posted
over 17 years
ago
I've just finished adding support for Asynchronous AGI to Asterisk-Java. AsyncAGI allows you to run AGI scripts through a Manager API connection.
The way AsyncAGI is supported by Asterisk-Java hides the differences in the underlying
... [More]
communication from the users of our library. Your AGI scripts developed for FastAGI will run with AsyncAGI without a change.
To make use of AsyncAGI add the following extension to you dialplan:
exten => 1234,1,Agi(agi:async)
Create a simple AGI script, pass it to AsyncAgiServer and register the AsyncAgiServer as a listener to a ManagerConnection:
public class SampleScript extends BaseAgiScript
{
public void service(AgiRequest request, AgiChannel channel) throws AgiException
{
channel.streamFile("tt-monkeys");
}
public static void main(String[] args) throws Exception
{
ManagerConnection connection;
AsyncAgiServer agiServer;
connection = new DefaultManagerConnection("localhost", "manager", "pa55w0rd");
agiServer = new AsyncAgiServer(new SampleScript());
connection.addEventListener(agiServer);
connection.login();
while (true)
{
Thread.sleep(1000L);
}
}
}
To run the sample you need the latest snapshot of Asterisk-Java 1.0.0 (at least 20080404.222056-117) and Asterisk 1.6.0.
References
Moy: Asterisk Asynchronous AGI
[Less]
|
Posted
over 17 years
ago
Thanks to Martin we now have a FAQ section available as part of our documentation.
The FAQ covers the questions we encounter on our mailing lists and that we think will be interesting to a more general audience. Please check them before asking
... [More]
questions to make sure you don't ask questions again that have already been answered before.
If you notice anything we've missed feel free to post a comment or join our mailing list and propose you addition.
References
Asterisk-Java Frequently Asked Questions
[Less]
|
Posted
over 17 years
ago
Andreas Neugebauer's submission to the snom XML Contest was a conferencing application for Asterisk:
XMLConference
uses the Asterisk Manager interface, and shows call-related information on the snom phone display and allows you to interact
... [More]
with these calls.
XMLConference uses Asterisk-Java's implementation of the Manager API to control Asterisk MeetMe rooms and make them available to the snom XML browser. It runs in a standard Java web container like Apache Tomcat.
References
XMLConference inlcudes source code and binaries
The snom XML Contest
[Less]
|